diff --git a/.gitignore b/.gitignore
index dd64e70b22..dd33a7dab3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ local.properties
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
+/buildSrc/build/
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 76a59bafa3..6527894333 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -12,6 +12,7 @@
+
@@ -199,4 +200,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 79ee123c2b..0f7bc519db 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -2,4 +2,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index c914631447..ce1b62532a 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -61,8 +61,8 @@
-
-
+
+
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 70b6edcebd..0000000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,187 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-kapt'
-apply plugin: 'com.google.gms.google-services'
-apply plugin: 'com.google.firebase.crashlytics'
-apply from: '../dependencies.gradle'
-
-android {
- compileSdkVersion 33
- defaultConfig {
- applicationId "com.tangem.wallet"
- versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1
- versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT"
- minSdkVersion 21
- targetSdkVersion 33
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
- buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
- buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
- }
- buildFeatures {
- compose true
- viewBinding true
- }
- buildTypes {
- release {
- debuggable false
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- signingConfig signingConfigs.debug
- buildConfigField 'String', environmentConfig.environment, '\"prod\"'
- buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
- buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
- }
- debug {
- debuggable true
- minifyEnabled false
- applicationIdSuffix ".dev"
- signingConfig signingConfigs.debug
- firebaseCrashlytics {
- mappingFileUploadEnabled false
- }
- buildConfigField 'String', environmentConfig.environment, '\"dev\"'
- buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true'
- buildConfigField 'Boolean', environmentConfig.logEnabled, 'true'
- }
- debug_beta {
- initWith release
- debuggable false
- versionNameSuffix "-beta"
- applicationIdSuffix ".debug"
- buildConfigField 'String', environmentConfig.environment, '\"prod\"'
- buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
- buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
- }
-
- }
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_1_8.toString()
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- coreLibraryDesugaringEnabled false
- }
- composeOptions {
- kotlinCompilerExtensionVersion '1.1.0'
- }
- packagingOptions {
- exclude 'lib/x86_64/darwin/libscrypt.dylib'
- exclude 'lib/x86_64/freebsd/libscrypt.so'
- exclude 'lib/x86_64/linux/libscrypt.so'
- }
-}
-
-repositories {
- maven { url "https://jitpack.io" }
-}
-
-dependencies {
- implementation fileTree(include: ['*.aar'], dir: 'libs')
- implementation implementation(project(path: ':domain'))
- implementation implementation(project(path: ':network'))
- implementation implementation(project(path: ':common'))
-
- implementation 'androidx.core:core-ktx:1.9.0'
- implementation 'androidx.appcompat:appcompat:1.5.1'
- implementation 'androidx.fragment:fragment-ktx:1.5.3'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- implementation 'com.google.android.material:material:1.6.1'
- implementation 'com.google.android.play:core:1.10.3'
- implementation 'com.google.android.play:core-ktx:1.8.1'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
-
- implementation "com.tangem:blockchain:${versions.tangem_blockchain_sdk}"
- implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tangem_card_sdk}"
- implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}"
-
- // WebView
- implementation "androidx.browser:browser:1.4.0"
-
- implementation 'com.jakewharton.timber:timber:4.7.1'
-
- //lifecycle
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
- implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
- implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
- implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
-
- implementation "org.rekotlin:rekotlin:1.0.4"
-
- // QR
- implementation 'com.google.zxing:core:3.3.3'
- implementation 'me.dm7.barcodescanner:zxing:1.9.8'
-
- // Camera
- implementation 'com.otaliastudios:cameraview:2.6.3'
-
- // Image Loading
- implementation 'io.coil-kt:coil:2.1.0'
- implementation 'io.coil-kt:coil-compose:2.1.0'
-
- // Firebase
- implementation platform('com.google.firebase:firebase-bom:26.0.0')
- implementation 'com.google.firebase:firebase-analytics-ktx'
- implementation 'com.google.firebase:firebase-crashlytics-ktx'
-
- // Analytics
- implementation 'com.appsflyer:af-android-sdk:6.5.1'
- implementation 'com.amplitude:android-sdk:2.36.1'
-
- // WalletConnect
- implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
-
- // Network and Json
- //TODO: refactoring: remove it when all network services moved to the network module
- implementation 'com.squareup.retrofit2:retrofit:2.8.1'
- implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
- implementation 'com.squareup.moshi:moshi:1.12.0'
- implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
-
- implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
- implementation("com.squareup.okhttp3:okhttp")
- implementation("com.squareup.okhttp3:logging-interceptor")
-
- // Support
- implementation("com.zendesk:chat:3.3.5")
- implementation("com.zendesk:messaging:5.2.4")
-
- //Crypto
- implementation "com.madgag.spongycastle:core:1.58.0.0"
-
- // animation
- implementation "com.airbnb.android:lottie:3.4.0"
-
- // Shopify
- implementation('com.shopify.mobilebuysdk:buy3:12.0.0') {
- exclude group: "com.shopify.graphql.support"
- exclude module: 'joda-time'
- }
-
- // Google Pay
- implementation 'com.google.android.gms:play-services-wallet:19.1.0'
-
- //Compose
- implementation 'androidx.activity:activity-compose:1.5.0'
- implementation 'androidx.compose.material:material:1.1.1'
- implementation 'androidx.compose.animation:animation:1.1.1'
- implementation 'androidx.compose.foundation:foundation:1.2.0-rc03'
- implementation 'androidx.compose.ui:ui:1.1.1'
- implementation 'androidx.compose.ui:ui-tooling:1.1.1'
- implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0"
- implementation "com.google.accompanist:accompanist-systemuicontroller:0.23.0"
-
- implementation "com.github.skydoves:androidveil:1.1.2"
-
- implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'
-
- // Shared preferences (encrypted)
- implementation 'at.favre.lib:armadillo:0.9.0'
-
- testImplementation 'junit:junit:4.13.2'
- testImplementation "com.google.truth:truth:1.1.3"
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
-
-}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000000..1876b0038c
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,208 @@
+plugins {
+ id("com.android.application")
+ kotlin("android")
+ kotlin("kapt")
+ id("com.google.gms.google-services")
+ id("com.google.firebase.crashlytics")
+ id("com.google.dagger.hilt.android")
+}
+
+android {
+ compileSdk = AppConfig.compileSdkVersion
+
+ defaultConfig {
+ applicationId = AppConfig.packageName
+ minSdk = AppConfig.minSdkVersion
+ targetSdk = AppConfig.targetSdkVersion
+
+ versionCode = if (project.hasProperty("versionCode")) {
+ (project.property("versionCode") as String).toInt()
+ } else {
+ AppConfig.versionCode
+ }
+
+ versionName = if (project.hasProperty("versionName")) {
+ project.property("versionName") as String
+ } else {
+ AppConfig.versionName
+ }
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildFeatures {
+ compose = true
+ viewBinding = true
+ }
+
+ buildTypes {
+ release {
+ isDebuggable = false
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+
+ BuildConfigFieldFactory(
+ fields = listOf(
+ Field.Environment("prod"),
+ Field.TestActionEnabled(false),
+ Field.LogEnabled(false),
+ ),
+ builder = ::buildConfigField,
+ ).create()
+ }
+
+ debug {
+ isDebuggable = true
+ isMinifyEnabled = false
+ applicationIdSuffix = ".dev"
+
+ configure {
+ // disable mapping file uploads (default=true if minifying)
+ mappingFileUploadEnabled = false
+ }
+
+ BuildConfigFieldFactory(
+ fields = listOf(
+ Field.Environment("dev"),
+ Field.TestActionEnabled(true),
+ Field.LogEnabled(true),
+ ),
+ builder = ::buildConfigField,
+ ).create()
+ }
+
+ create("debug_beta") {
+ initWith(getByName("release"))
+ versionNameSuffix = "-beta"
+ applicationIdSuffix = ".debug"
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8.toString()
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ isCoreLibraryDesugaringEnabled = false
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose
+ }
+
+ packagingOptions {
+ resources.excludes += "lib/x86_64/darwin/libscrypt.dylib"
+ resources.excludes += "lib/x86_64/freebsd/libscrypt.so"
+ resources.excludes += "lib/x86_64/linux/libscrypt.so"
+ resources.excludes += "META-INF/gradle/incremental.annotation.processors"
+ }
+}
+
+repositories {
+ maven(url = "https://jitpack.io")
+}
+
+dependencies {
+ implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
+ implementation(project(":domain"))
+ implementation(project(":common"))
+ implementation(project(":core:res"))
+ implementation(project(":core:ui"))
+ implementation(project(":core:datasource"))
+ implementation(project(":core:utils"))
+ implementation(project(":libs:crypto"))
+ implementation(project(":libs:auth"))
+
+ /** Features */
+ implementation(project(":features:referral:presentation"))
+ implementation(project(":features:referral:domain"))
+ implementation(project(":features:referral:data"))
+ implementation(project(":features:swap:presentation"))
+ implementation(project(":features:swap:domain"))
+ implementation(project(":features:swap:data"))
+
+ /** AndroidX libraries */
+ implementation(AndroidX.coreKtx)
+ implementation(AndroidX.appCompat)
+ implementation(AndroidX.fragmentKtx)
+ implementation(AndroidX.constraintLayout)
+ implementation(AndroidX.browser)
+ implementation(AndroidX.lifecycleRuntimeKtx)
+ implementation(AndroidX.lifecycleCommonJava8)
+ implementation(AndroidX.lifecycleViewModelKtx)
+ implementation(AndroidX.lifecycleLiveDataKtx)
+ implementation(AndroidX.activityCompose)
+
+ /** Compose libraries */
+ implementation(Compose.material)
+ implementation(Compose.animation)
+ implementation(Compose.foundation)
+ implementation(Compose.ui)
+ implementation(Compose.uiTooling)
+ implementation(Compose.coil)
+
+ /** Firebase libraries */
+ implementation(platform(Firebase.bom))
+ implementation(Firebase.firebaseAnalytics)
+ implementation(Firebase.firebaseCrashlytics)
+
+ /** Tangem libraries */
+ implementation(Tangem.blockchain) {
+ exclude(module = "joda-time")
+ }
+ implementation(Tangem.cardCore)
+ implementation(Tangem.cardAndroid) {
+ exclude(module = "joda-time")
+ }
+
+ /** DI */
+ implementation(Library.hilt)
+ kapt(Library.hiltKapt)
+
+ /** Other libraries */
+ implementation(Library.materialComponent)
+ implementation(Library.googlePlayCore)
+ implementation(Library.googlePlayCoreKtx)
+ coreLibraryDesugaring(Library.desugarJdkLibs)
+ implementation(Library.timber)
+ implementation(Library.reKotlin)
+ implementation(Library.zxingQrCore)
+ implementation(Library.zxingQrBarcodeScanner)
+ implementation(Library.otaliastudiosCameraView)
+ implementation(Library.coil)
+ implementation(Library.appsflyer)
+ implementation(Library.amplitude)
+ implementation(Library.kotsonGsonExt)
+ //TODO: refactoring: remove it when all network services moved to the datasource module
+ implementation(Library.retrofit)
+ implementation(Library.retrofitMoshiConverter)
+ implementation(Library.moshi)
+ implementation(Library.moshiKotlin)
+ implementation(Library.okHttp)
+ implementation(Library.okHttpLogging)
+ implementation(Library.zendeskChat)
+ implementation(Library.zendeskMessaging)
+ implementation(Library.spongecastleCryptoCore)
+ implementation(Library.lottie)
+ implementation(Library.shopifyBuySdk) {
+ exclude(group = "com.shopify.graphql.support")
+ exclude(module = "joda-time")
+ }
+ implementation(Library.accompanistAppCompatTheme)
+ implementation(Library.accompanistSystemUiController)
+ implementation(Library.xmlShimmer)
+ implementation(Library.viewBindingDelegate)
+ implementation(Library.armadillo)
+ implementation(Library.googlePlayServicesWallet)
+ implementation(Library.composeShimmer)
+ implementation(Library.mviCoreWatcher)
+
+ /** Testing libraries */
+ testImplementation(Test.junit)
+ testImplementation(Test.truth)
+ androidTestImplementation(Test.junitAndroidExt)
+ androidTestImplementation(Test.espresso)
+}
\ No newline at end of file
diff --git a/app/libs/walletconnect-1.5.5.aar b/app/libs/walletconnect-1.5.5.aar
deleted file mode 100644
index 4e2f1b4fb7..0000000000
Binary files a/app/libs/walletconnect-1.5.5.aar and /dev/null differ
diff --git a/app/libs/walletconnect-1.5.6.aar b/app/libs/walletconnect-1.5.6.aar
new file mode 100644
index 0000000000..30c576fb89
Binary files /dev/null and b/app/libs/walletconnect-1.5.6.aar differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index f1b424510d..2f9dc5a47e 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 874783a15d..e7d11764b4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,11 +34,10 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
- android:allowBackup="true"
- android:fullBackupContent="true"
+ android:allowBackup="false"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="GoogleAppIndexingWarning"
- tools:replace="android:fullBackupContent">
+ tools:replace="android:allowBackup">
+
+
+
+
+
+
+
+
+
+
+
@@ -142,5 +152,4 @@
-
diff --git a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
new file mode 100644
index 0000000000..16a05117ac
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
@@ -0,0 +1,8 @@
+package com.tangem.tap
+
+import android.content.Intent
+import androidx.activity.result.ActivityResultLauncher
+
+interface ActivityResultCaller {
+ val activityResultLauncher: ActivityResultLauncher?
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt
index 806769df5f..9664f38966 100644
--- a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt
+++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt
@@ -2,11 +2,18 @@ package com.tangem.tap
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
+import android.content.Intent
import android.os.Bundle
-import java.util.WeakHashMap
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import java.util.*
import kotlin.reflect.KClass
-class ForegroundActivityObserver {
+class ForegroundActivityObserver : ActivityResultCaller {
+ override var activityResultLauncher: ActivityResultLauncher? = null
+ private set
+
private val activities = WeakHashMap, Activity>()
val foregroundActivity: Activity?
@@ -15,25 +22,38 @@ class ForegroundActivityObserver {
.firstOrNull()
?.value
- val callbacks get() = Callbacks()
+ internal val callbacks: ActivityLifecycleCallbacks
+ get() = Callbacks()
+
+ internal inner class Callbacks : ActivityLifecycleCallbacks {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ activityResultLauncher = (activity as? AppCompatActivity)?.registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult(),
+ ) {
+ /* no-op */
+ }
+ }
- inner class Callbacks : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
activities[activity::class] = activity
}
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity::class)
+ if (activities.isEmpty()) {
+ activityResultLauncher = null
+ }
}
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
- }
override fun onActivityStarted(activity: Activity) {
}
+
override fun onActivityPaused(activity: Activity) {
}
+
override fun onActivityStopped(activity: Activity) {
}
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
}
diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
new file mode 100644
index 0000000000..426f49ca25
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
@@ -0,0 +1,117 @@
+package com.tangem.tap
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.tangem.tap.common.extensions.dispatchOnMain
+import com.tangem.tap.common.redux.navigation.AppScreen
+import com.tangem.tap.common.redux.navigation.NavigationAction
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import kotlin.time.Duration
+
+internal class LockUserWalletsTimer(
+ owner: LifecycleOwner,
+ private val duration: Duration = with(Duration) { 10.minutes },
+) : LifecycleOwner by owner,
+ DefaultLifecycleObserver {
+
+ private var delayJob: Job? = null
+ set(value) {
+ field?.cancel()
+ field = value
+ }
+
+ init {
+ lifecycle.addObserver(this)
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ Timber.d(
+ """
+ Owner resumed
+ |- Was stopped: ${preferencesStorage.wasApplicationStopped}
+ |- Need to open welcome screen: ${preferencesStorage.shouldOpenWelcomeScreenOnResume}
+ """.trimIndent(),
+ )
+ preferencesStorage.wasApplicationStopped = false
+ start()
+ if (preferencesStorage.shouldOpenWelcomeScreenOnResume) {
+ store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
+ preferencesStorage.shouldOpenWelcomeScreenOnResume = false
+ }
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ Timber.d("Owner stopped")
+ preferencesStorage.wasApplicationStopped = true
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ Timber.d("Owner destroyed")
+ stop()
+ }
+
+ fun restart() {
+ if (delayJob == null) return
+ Timber.d(
+ """
+ Timer restart
+ |- Duration millis: ${duration.inWholeMilliseconds}
+ """.trimIndent(),
+ )
+ start(log = false)
+ }
+
+ private fun start(log: Boolean = true) {
+ if (log) {
+ Timber.d(
+ """
+ Timer start
+ |- Duration millis: ${duration.inWholeMilliseconds}
+ """.trimIndent(),
+ )
+ }
+ delayJob = createDelayJob()
+ }
+
+ private fun stop(log: Boolean = true) {
+ if (log) {
+ Timber.d(
+ """
+ Timer stop
+ |- Was started: ${delayJob?.isActive ?: false}
+ """.trimIndent(),
+ )
+ }
+ delayJob = null
+ }
+
+ private fun createDelayJob(): Job = lifecycleScope.launch(Dispatchers.Default) {
+ val startTime = System.currentTimeMillis()
+ delay(duration)
+ if (isActive) {
+ val userWalletsListManager = userWalletsListManagerSafe ?: return@launch
+ if (userWalletsListManager.hasSavedUserWallets) {
+ val currentTime = System.currentTimeMillis()
+ Timber.d(
+ """
+ Finished
+ |- App is stopped: ${preferencesStorage.wasApplicationStopped}
+ |- Millis passed: ${currentTime - startTime}
+ """.trimIndent(),
+ )
+ userWalletsListManager.lock()
+ if (preferencesStorage.wasApplicationStopped) {
+ preferencesStorage.shouldOpenWelcomeScreenOnResume = true
+ } else {
+ store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt
index e557e59247..5ef7c985a6 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -15,30 +15,39 @@ import com.tangem.operations.backup.BackupService
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
-import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
+import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.NotificationsHandler
-import com.tangem.tap.common.redux.global.AndroidResources
-import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.GooglePayService
import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
+import com.tangem.tap.domain.userWalletList.UserWalletsListManager
+import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.features.shop.redux.ShopAction
+import com.tangem.tap.features.welcome.redux.WelcomeAction
+import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
+import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.lang.ref.WeakReference
+import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
+lateinit var userWalletsListManager: UserWalletsListManager
+internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
+ private set
+var userWalletsListManagerSafe: UserWalletsListManager? = null
+ private set
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
@@ -49,11 +58,14 @@ private val mainCoroutineContext: CoroutineContext
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
val mainScope = CoroutineScope(mainCoroutineContext)
+@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
+ @Inject
+ lateinit var appStateHolder: AppStateHolder
+
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
- private val intentHandler = IntentHandler()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
private val onActivityResultCallbacks = mutableListOf()
@@ -66,9 +78,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
+ appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(tangemSdk, this)
+ userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
+ context = applicationContext,
+ tangemSdkManager = tangemSdkManager,
+ )
+ userWalletsListManagerSafe = userWalletsListManager
+ lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
- store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this),
@@ -76,17 +94,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
)
}
- private fun getAndroidResources(): AndroidResources {
- return AndroidResources(
- AndroidResources.RString(
- R.string.copy_toast_msg,
- R.string.details_notification_erase_wallet_not_possible,
- ),
- )
- }
-
private fun systemActions() {
-
WindowCompat.setDecorFitsSystemWindows(window, false)
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)
@@ -110,14 +118,23 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
val shopOpened = store.state.shopState.total != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) {
- store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
+ if (userWalletsListManager.hasSavedUserWallets) {
+ store.dispatchOnMain(WelcomeAction.HandleDeepLink(intent))
+ store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome))
+ } else {
+ store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home))
+ intentHandler.handleWalletConnectLink(intent)
+ }
}
- intentHandler.handleIntent(intent)
+ intentHandler.handleBackgroundScan(intent)
+ intentHandler.handleSellCurrencyCallback(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
- intentHandler.handleIntent(intent)
+ intentHandler.handleBackgroundScan(intent)
+ intentHandler.handleSellCurrencyCallback(intent)
+ intentHandler.handleWalletConnectLink(intent)
}
override fun onStart() {
@@ -174,4 +191,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
onActivityResultCallbacks.remove(callback)
}
+
+ override fun onUserInteraction() {
+ super.onUserInteraction()
+
+ lockUserWalletsTimer?.restart()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt
index c0d6e566b4..0d483b43ee 100644
--- a/app/src/main/java/com/tangem/tap/TapApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TapApplication.kt
@@ -7,18 +7,30 @@ import coil.ImageLoader
import coil.ImageLoaderFactory
import com.tangem.Log
import com.tangem.LogFormat
+import com.tangem.blockchain.common.BlockchainSdkConfig
+import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.common.json.MoshiJsonConverter
+import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
-import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.AssetReader
-import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandlerBuilder
+import com.tangem.tap.common.IntentHandler
+import com.tangem.tap.common.analytics.Analytics
+import com.tangem.tap.common.analytics.AnalyticsFactory
+import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
+import com.tangem.tap.common.analytics.filters.BasicSignInFilter
+import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
+import com.tangem.tap.common.analytics.filters.ShopPurchasedEventFilter
+import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
+import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
+import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
+import com.tangem.tap.common.moshi.BigDecimalAdapter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
@@ -28,47 +40,112 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.UserTokensRepository
+import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
+import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
+import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
+import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
+import com.tangem.tap.domain.walletStores.WalletStoresManager
+import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
+import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
+import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
+import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
+import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
+import com.tangem.tap.persistence.CardBalanceStateAdapter
import com.tangem.tap.persistence.PreferencesStorage
+import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.BuildConfig
import com.zendesk.logger.Logger
+import dagger.hilt.android.HiltAndroidApp
import org.rekotlin.Store
import timber.log.Timber
import zendesk.chat.Chat
+import javax.inject.Inject
-val store = Store(
- reducer = ::appReducer,
- middleware = AppState.getMiddleware(),
- state = AppState(),
-)
+lateinit var store: Store
lateinit var foregroundActivityObserver: ForegroundActivityObserver
+lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesStorage
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
+private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
+private val walletManagersRepository by lazy {
+ WalletManagersRepository.provideDefaultImplementation(
+ walletManagerFactory = WalletManagerFactory(
+ blockchainSdkConfig = store.state.globalState.configManager
+ ?.config
+ ?.blockchainSdkConfig
+ ?: BlockchainSdkConfig(),
+ ),
+ )
+}
+private val walletAmountsRepository by lazy {
+ WalletAmountsRepository.provideDefaultImplementation(
+ tangemTechService = store.state.domainNetworks.tangemTechService,
+ )
+}
+val walletStoresManager by lazy {
+ WalletStoresManager.provideDefaultImplementation(
+ userTokensRepository = userTokensRepository,
+ walletStoresRepository = walletStoresRepository,
+ walletManagersRepository = walletManagersRepository,
+ walletAmountsRepository = walletAmountsRepository,
+ appCurrencyProvider = { store.state.globalState.appCurrency },
+ )
+}
+val walletCurrenciesManager by lazy {
+ WalletCurrenciesManager.provideDefaultImplementation(
+ userTokensRepository = userTokensRepository,
+ walletStoresRepository = walletStoresRepository,
+ walletManagersRepository = walletManagersRepository,
+ walletAmountsRepository = walletAmountsRepository,
+ appCurrencyProvider = { store.state.globalState.appCurrency },
+ )
+}
+val totalFiatBalanceCalculator by lazy {
+ TotalFiatBalanceCalculator.provideDefaultImplementation()
+}
+val intentHandler by lazy { IntentHandler() }
+
+@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {
+ @Inject
+ lateinit var appStateHolder: AppStateHolder
+
override fun onCreate() {
super.onCreate()
+ store = Store(
+ reducer = { action, state ->
+ appReducer(action, state, appStateHolder)
+ },
+ middleware = AppState.getMiddleware(),
+ state = AppState(),
+ )
+ appStateHolder.mainStore = store
+
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
foregroundActivityObserver = ForegroundActivityObserver()
+ activityResultCaller = foregroundActivityObserver
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
+ initMoshiConverter()
DomainLayer.init()
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
walletConnectRepository = WalletConnectRepository(this)
assetReader = AndroidAssetReader(this)
- val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.defaultMoshi())
+ val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.INSTANCE.moshi)
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
@@ -78,6 +155,19 @@ class TapApplication : Application(), ImageLoaderFactory {
context = this,
tangemTechService = store.state.domainNetworks.tangemTechService,
)
+ appStateHolder.userTokensRepository = userTokensRepository
+ }
+
+ //todo refactor: move to datasource and provide via DI
+ private fun initMoshiConverter() {
+ fun appAdapters(): List = listOf(
+ BigDecimalAdapter(),
+ CardBalanceStateAdapter(),
+ )
+ MoshiConverter.reInitInstance(
+ adapters = appAdapters() + MoshiJsonConverter.getTangemSdkAdapters(),
+ typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
+ )
}
override fun newImageLoader(): ImageLoader {
@@ -102,14 +192,23 @@ class TapApplication : Application(), ImageLoaderFactory {
}
private fun initAnalytics(application: Application, config: Config) {
- val globalAnalyticsHandler = GlobalAnalyticsEventHandlerBuilder(
+ val factory = AnalyticsFactory()
+ factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
+ factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
+ factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
+
+ factory.addFilter(ShopPurchasedEventFilter())
+ factory.addFilter(BasicSignInFilter())
+ factory.addFilter(BasicTopUpFilter(preferencesStorage.toppedUpWalletStorage))
+
+ val buildData = AnalyticsHandlerBuilder.Data(
application = application,
config = config,
isDebug = BuildConfig.DEBUG,
logConfig = LogConfig.analyticsHandlers,
- jsonConverter = MoshiJsonConverter.INSTANCE,
- ).default()
- store.dispatch(GlobalAction.SetGlobalAnalyticsHandler(globalAnalyticsHandler))
+ jsonConverter = MoshiConverter.INSTANCE,
+ )
+ factory.build(Analytics, buildData)
}
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) {
diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt
index a1739329b5..f25d1ddf4c 100644
--- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt
+++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt
@@ -76,13 +76,13 @@ class DialogManager : StoreSubscriber {
CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
- titleRes = R.string.wallet_connect,
- messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
+ titleRes = R.string.wallet_connect_title,
+ messageRes = R.string.wallet_connect_scanner_error_not_valid_card,
context = context,
)
is WalletConnectDialog.AddNetwork ->
SimpleAlertDialog.create(
- titleRes = R.string.wallet_connect,
+ titleRes = R.string.wallet_connect_title,
message = context.getString(
R.string.wallet_connect_network_not_found_format,
state.dialog.network,
@@ -91,14 +91,14 @@ class DialogManager : StoreSubscriber {
)
is WalletConnectDialog.OpeningSessionRejected -> {
SimpleAlertDialog.create(
- titleRes = R.string.wallet_connect,
+ titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_same_wcuri,
context = context,
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
- titleRes = R.string.wallet_connect,
+ titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_timeout,
context = context,
)
@@ -123,7 +123,7 @@ class DialogManager : StoreSubscriber {
)
is WalletConnectDialog.UnsupportedNetwork ->
SimpleAlertDialog.create(
- titleRes = R.string.wallet_connect,
+ titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
context = context,
)
@@ -153,7 +153,7 @@ class DialogManager : StoreSubscriber {
primaryButtonAction = state.dialog.onOk,
)
is WalletDialog.RussianCardholdersWarningDialog ->
- RussianCardholdersWarningBottomSheetDialog(context, state.dialog.topUpUrl)
+ RussianCardholdersWarningBottomSheetDialog(context, state.dialog.data)
else -> null
}
dialog?.show()
diff --git a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt
index 1e41053369..4ac3547aa0 100644
--- a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt
+++ b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt
@@ -3,6 +3,7 @@ package com.tangem.tap.common
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
+import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.walletconnect.WalletConnectManager
@@ -14,18 +15,24 @@ import timber.log.Timber
class IntentHandler {
- private val TRANSACTION_ID_PARAM = "transactionId"
- private val CURRENCY_CODE_PARAM = "baseCurrencyCode"
- private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
- private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
-
- fun handleIntent(intent: Intent?) {
- handleBackgroundScan(intent)
- handleWalletConnectLink(intent)
- handleSellCurrencyCallback(intent)
+ fun handleWalletConnectLink(intent: Intent?) {
+ val wcUri = when (intent?.scheme) {
+ WalletConnectManager.WC_SCHEME -> {
+ intent.data?.toString()
+ }
+ TANGEM_SCHEME -> {
+ intent.data?.toString()?.removePrefixOrNull(TANGEM_WC_PREFIX)
+ }
+ else -> {
+ null
+ }
+ }
+ if (wcUri != null) {
+ store.dispatch(WalletConnectAction.HandleDeepLink(wcUri))
+ }
}
- private fun handleBackgroundScan(intent: Intent?) {
+ fun handleBackgroundScan(intent: Intent?) {
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
)
@@ -40,13 +47,7 @@ class IntentHandler {
}
}
- private fun handleWalletConnectLink(intent: Intent?) {
- if (intent?.scheme == WalletConnectManager.WC_SCHEME) {
- store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString()))
- }
- }
-
- private fun handleSellCurrencyCallback(intent: Intent?) {
+ fun handleSellCurrencyCallback(intent: Intent?) {
try {
val transactionID =
intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return
@@ -60,16 +61,25 @@ class IntentHandler {
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
- store.dispatch(WalletAction.TradeCryptoAction.SendCrypto(
- currencyId = currency,
- amount = amount,
- destinationAddress = destinationAddress,
- transactionId = transactionID
- ))
+ store.dispatch(
+ WalletAction.TradeCryptoAction.SendCrypto(
+ currencyId = currency,
+ amount = amount,
+ destinationAddress = destinationAddress,
+ transactionId = transactionID,
+ ),
+ )
} catch (exception: Exception) {
Timber.d("Not MoonPay URL")
}
}
-
+ companion object {
+ private const val TRANSACTION_ID_PARAM = "transactionId"
+ private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
+ private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
+ private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
+ private const val TANGEM_SCHEME = "tangem"
+ private const val TANGEM_WC_PREFIX = "tangem://wc?uri="
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt
new file mode 100644
index 0000000000..8a6ee52f27
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt
@@ -0,0 +1,93 @@
+package com.tangem.tap.common.analytics
+
+import com.tangem.domain.common.FeatureCoroutineExceptionHandler
+import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
+import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
+import com.tangem.tap.common.analytics.api.AnalyticsFilterHolder
+import com.tangem.tap.common.analytics.api.AnalyticsHandler
+import com.tangem.tap.common.analytics.api.AnalyticsHandlerHolder
+import com.tangem.tap.common.analytics.api.ParamsInterceptor
+import com.tangem.tap.common.analytics.api.ParamsInterceptorHolder
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executors
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface GlobalAnalyticsEventHandler : AnalyticsEventHandler,
+ AnalyticsHandlerHolder,
+ AnalyticsFilterHolder,
+ ParamsInterceptorHolder
+
+object Analytics : GlobalAnalyticsEventHandler {
+
+ private val analyticsScope: CoroutineScope by lazy { createScope() }
+
+ private val handlers = mutableMapOf()
+ private val paramsInterceptors = mutableMapOf()
+ private val analyticsFilters = mutableSetOf()
+
+ private val analyticsHandlers: List
+ get() = handlers.values.toList()
+
+ override fun addHandler(name: String, handler: AnalyticsHandler) {
+ handlers[name] = handler
+ }
+
+ override fun removeHandler(name: String): AnalyticsHandler? {
+ return handlers.remove(name)
+ }
+
+ override fun addFilter(filter: AnalyticsEventFilter) {
+ analyticsFilters.add(filter)
+ }
+
+ override fun removeFilter(filter: AnalyticsEventFilter): Boolean {
+ return analyticsFilters.remove(filter)
+ }
+
+ override fun addParamsInterceptor(interceptor: ParamsInterceptor) {
+ paramsInterceptors[interceptor.id()] = interceptor
+ }
+
+ override fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor? {
+ return paramsInterceptors.remove(interceptor.id())
+ }
+
+ override fun send(event: AnalyticsEvent) {
+ analyticsScope.launch {
+ event.params = applyParamsInterceptors(event)
+ val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }
+
+ when {
+ eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) }
+ eventFilter.canBeSent(event) -> {
+ analyticsHandlers
+ .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) }
+ .forEach { handler -> handler.send(event) }
+ }
+ }
+ }
+ }
+
+ private fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap {
+ val interceptedParams = event.params.toMutableMap()
+ paramsInterceptors.values
+ .filter { it.canBeAppliedTo(event) }
+ .forEach { it.intercept(interceptedParams) }
+
+ return interceptedParams
+ }
+
+ private fun createScope(): CoroutineScope {
+ val name = "Analytics"
+ val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
+ val exHandler = FeatureCoroutineExceptionHandler.create(name)
+ return CoroutineScope(SupervisorJob() + dispatcher + CoroutineName(name) + exHandler)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt
deleted file mode 100644
index 41cc0d9b96..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.tangem.tap.common.analytics
-
-enum class AnalyticsEventAnOld(val event: String) {
- CARD_IS_SCANNED("card_is_scanned"),
- TRANSACTION_IS_SENT("transaction_is_sent"),
- READY_TO_SCAN("ready_to_scan"),
- DEMO_MODE_ACTIVATED("demo_mode_activated"),
-
- APP_RATING_DISPLAYED("rate_app_warning_displayed"),
- APP_RATING_DISMISS("dismiss_rate_app_warning"),
- APP_RATING_NEGATIVE("negative_rate_app_feedback"),
- APP_RATING_POSITIVE("positive_rate_app_feedback"),
-
- WC_SUCCESS_RESPONSE("wallet_connect_success_response"),
- WC_INVALID_REQUEST("wallet_connect_invalid_request"),
- WC_NEW_SESSION("wallet_connect_new_session"),
- WC_SESSION_DISCONNECTED("wallet_connect_session_disconnected"),
-
- GET_CARD("get_card"),
-}
-
-enum class AnalyticsParamAnOld(val param: String) {
- BLOCKCHAIN("blockchain"),
- CARD_ID("cardId"),
- BATCH_ID("batch_id"),
- FIRMWARE("firmware"),
- ACTION("action"),
- ERROR_DESCRIPTION("error_description"),
- ERROR_CODE("error_code"),
- NEW_SECURITY_OPTION("new_security_option"),
- ERROR_KEY("Tangem SDK error key"),
-
- WALLET_CONNECT_ACTION("wallet_connect_action"),
- WALLET_CONNECT_REQUEST("wallet_connect_request"),
- WALLET_CONNECT_DAPP_URL("wallet_connect_dapp_url"),
-
- SOURCE("source"),
-}
-
-enum class GetCardSourceParamsAnOld(val param: String) {
- WELCOME("welcome"),
- ONBOARDING_BUY_MORE("wallet_onboaring_buy_more_cards"),
- ONBOARDING("wallet_onboarding")
-}
-
-object AnalyticsAnOld {
- enum class ActionToLog(val key: String) {
- Scan("tap_scan_task"),
- SendTransaction("send_transaction"),
- WalletConnectSign("wallet_connect_personal_sign"),
- WalletConnectTransaction("wallet_connect_tx_sign"),
- ReadPinSettings("read_pin_settings"),
- ChangeSecOptions("change_sec_options"),
- CreateWallet("create_wallet"),
- PurgeWallet("purge_wallet"),
- WriteIssuerData("write_issuer_data"),
- }
-
- enum class WcSessionEvent { Disconnect, Connect }
-
- enum class WcAction { PersonalSign, SignTransaction, SendTransaction }
-
- sealed class WcAnalyticsEvent {
- data class Error(val error: Throwable, val action: WcAction?) :
- WcAnalyticsEvent()
-
- data class Session(val event: WcSessionEvent, val url: String?) : WcAnalyticsEvent()
- data class Action(val action: WcAction) : WcAnalyticsEvent()
- data class InvalidRequest(val json: String?) : WcAnalyticsEvent()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsFactory.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsFactory.kt
new file mode 100644
index 0000000000..8936adfc3c
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsFactory.kt
@@ -0,0 +1,37 @@
+package com.tangem.tap.common.analytics
+
+import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
+import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
+import com.tangem.tap.common.analytics.api.ParamsInterceptor
+
+/**
+[REDACTED_AUTHOR]
+ */
+class AnalyticsFactory {
+
+ private val builders = mutableListOf()
+ private val filters = mutableListOf()
+ private val interceptors = mutableListOf()
+
+ fun addHandlerBuilder(builder: AnalyticsHandlerBuilder) {
+ builders.add(builder)
+ }
+
+ fun addFilter(filter: AnalyticsEventFilter) {
+ filters.add(filter)
+ }
+
+ fun addParamsInterceptor(interceptor: ParamsInterceptor) {
+ interceptors.add(interceptor)
+ }
+
+ fun build(analytics: Analytics, data: AnalyticsHandlerBuilder.Data) {
+ builders.mapNotNull { it.build(data) }.forEach { analytics.addHandler(it.id(), it) }
+ filters.forEach { analytics.addFilter(it) }
+ interceptors.forEach { analytics.addParamsInterceptor(it) }
+
+ builders.clear()
+ filters.clear()
+ interceptors.clear()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt
deleted file mode 100644
index 6fd6fc7dc8..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.tangem.tap.common.analytics
-
-
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt
deleted file mode 100644
index 8e2b439e08..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.tangem.tap.common.analytics
-
-import android.app.Application
-import com.tangem.common.json.MoshiJsonConverter
-import com.tangem.domain.common.AnalyticsHandlersLogConfig
-import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
-import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
-import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeClient
-import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeLogClient
-import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
-import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerClient
-import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerLogClient
-import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
-import com.tangem.tap.common.analytics.handlers.firebase.FirebaseClient
-import com.tangem.tap.common.analytics.handlers.firebase.FirebaseLogClient
-import com.tangem.tap.domain.configurable.config.Config
-
-/**
-[REDACTED_AUTHOR]
- */
-class GlobalAnalyticsEventHandlerBuilder(
- val application: Application,
- val config: Config,
- val isDebug: Boolean,
- val logConfig: AnalyticsHandlersLogConfig,
- val jsonConverter: MoshiJsonConverter,
-) {
-
- private val analyticHandlers = mutableListOf()
-
- fun build(): GlobalAnalyticsEventHandler {
- return GlobalAnalyticsHandler(analyticHandlers.toList())
- }
-
- fun default(): GlobalAnalyticsEventHandler {
- addFirebaseAnalytics()
- addAppsFlyer()
- addAmplitude()
-
- return GlobalAnalyticsHandler(analyticHandlers.toList())
- }
-
- fun addFirebaseAnalytics(): GlobalAnalyticsEventHandlerBuilder {
- if (isDebug) {
- if (logConfig.firebase) FirebaseLogClient(createLogger("FirebaseAnalyticsHandler")) else null
- } else {
- FirebaseClient()
- }?.let {
- analyticHandlers.add(FirebaseAnalyticsHandler(it))
- }
- return this
- }
-
- fun addAppsFlyer(): GlobalAnalyticsEventHandlerBuilder {
- if (isDebug) {
- if (logConfig.appsFlyer) AppsFlyerLogClient(createLogger("AppsFlyerAnalyticsHandler")) else null
- } else {
- AppsFlyerClient(application, config.appsFlyerDevKey)
- }?.let {
- analyticHandlers.add(AppsFlyerAnalyticsHandler(it))
- }
- return this
- }
-
- fun addAmplitude(): GlobalAnalyticsEventHandlerBuilder {
- if (isDebug) {
- if (logConfig.firebase) AmplitudeLogClient(createLogger("AmplitudeAnalyticsHandler")) else null
- } else {
- AmplitudeClient(application, config.amplitudeApiKey)
- }?.let {
- analyticHandlers.add(AmplitudeAnalyticsHandler(it))
- }
- return this
- }
-
- private fun createLogger(name: String): AnalyticsEventsLogger {
- return AnalyticsEventsLogger(name, jsonConverter)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt
deleted file mode 100644
index d1bef7d01b..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-package com.tangem.tap.common.analytics
-
-import com.shopify.buy3.Storefront
-import com.tangem.blockchain.common.BlockchainError
-import com.tangem.common.card.Card
-import com.tangem.common.core.TangemSdkError
-import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
-import com.tangem.tap.common.analytics.api.BlockchainSdkErrorEventHandler
-import com.tangem.tap.common.analytics.api.CardSdkErrorEventHandler
-import com.tangem.tap.common.analytics.api.ErrorEventHandler
-import com.tangem.tap.common.analytics.api.ErrorEventLogger
-import com.tangem.tap.common.analytics.api.SdkErrorEventHandler
-import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler
-import com.tangem.tap.common.analytics.events.AnalyticsEvent
-import com.tangem.tap.common.extensions.filterNotNull
-
-interface GlobalAnalyticsEventHandler : AnalyticsEventHandler,
- ErrorEventHandler,
- SdkErrorEventHandler,
- ShopifyOrderEventHandler {
-
- fun attachToAllEvents(key: String, value: String)
-
- companion object {
- fun stub(): GlobalAnalyticsEventHandler {
- return object : GlobalAnalyticsEventHandler {
- override fun attachToAllEvents(key: String, value: String) {}
- override fun handleEvent(event: String, params: Map) {}
- override fun handleErrorEvent(error: Throwable, params: Map) {}
- override fun handleCardSdkErrorEvent(
- error: TangemSdkError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map,
- card: Card?,
- ) {
- }
-
- override fun handleBlockchainSdkErrorEvent(
- error: BlockchainError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map,
- card: Card?,
- ) {
- }
-
- override fun handleShopifyOrderEvent(order: Storefront.Order) {}
- }
- }
- }
-}
-
-class GlobalAnalyticsHandler(
- private val analyticsHandlers: List,
-) : GlobalAnalyticsEventHandler {
-
- private val attachToAllEventsParams: MutableMap = mutableMapOf()
-
- override fun attachToAllEvents(key: String, value: String) {
- attachToAllEventsParams[key] = value
- }
-
- override fun handleEvent(event: String, params: Map) {
- analyticsHandlers.forEach { it.handleEvent(event, params) }
- }
-
- override fun handleAnalyticsEvent(
- event: AnalyticsEvent,
- card: Card?,
- blockchain: String?,
- ) {
- analyticsHandlers.forEach { it.handleAnalyticsEvent(event, card, blockchain) }
- }
-
- override fun handleAnalyticsEvent(
- event: AnalyticsEventAnOld,
- params: Map,
- card: Card?,
- blockchain: String?,
- ) {
- analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) }
- }
-
- override fun handleErrorEvent(error: Throwable, params: Map) {
- analyticsHandlers.filterIsInstance().forEach {
- it.logErrorEvent(error, params)
- }
- }
-
- override fun handleCardSdkErrorEvent(
- error: TangemSdkError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map,
- card: Card?,
- ) {
- analyticsHandlers.filterIsInstance().forEach {
- it.handleCardSdkErrorEvent(error, action, params, card)
- }
- }
-
- override fun handleBlockchainSdkErrorEvent(
- error: BlockchainError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map,
- card: Card?,
- ) {
- analyticsHandlers.filterIsInstance().forEach {
- it.handleBlockchainSdkErrorEvent(error, action, params, card)
- }
- }
-
- override fun handleShopifyOrderEvent(order: Storefront.Order) {
- analyticsHandlers.filterIsInstance().forEach {
- it.handleShopifyOrderEvent(order)
- }
- }
-
- override fun prepareParams(card: Card?, blockchain: String?, params: Map): Map {
- return super.prepareParams(card, blockchain, params).toMutableMap().apply {
- putAll(attachToAllEventsParams)
- }
- }
-}
-
-fun GlobalAnalyticsEventHandler.logWcEvent(event: AnalyticsAnOld.WcAnalyticsEvent) {
- when (event) {
- is AnalyticsAnOld.WcAnalyticsEvent.Action -> {
- handleAnalyticsEvent(
- event = AnalyticsEventAnOld.WC_SUCCESS_RESPONSE,
- params = mapOf(
- AnalyticsParamAnOld.WALLET_CONNECT_ACTION.param to event.action.name,
- ),
- )
- }
- is AnalyticsAnOld.WcAnalyticsEvent.Error -> {
- val params = mapOf(
- AnalyticsParamAnOld.WALLET_CONNECT_ACTION.param to event.action?.name,
- AnalyticsParamAnOld.ERROR_DESCRIPTION.param to event.error.message,
- ).filterNotNull()
- handleErrorEvent(event.error, params)
- }
- is AnalyticsAnOld.WcAnalyticsEvent.InvalidRequest ->
- handleAnalyticsEvent(
- event = AnalyticsEventAnOld.WC_INVALID_REQUEST,
- params = mapOf(
- AnalyticsParamAnOld.WALLET_CONNECT_REQUEST.param to event.json,
- ).filterNotNull(),
- )
- is AnalyticsAnOld.WcAnalyticsEvent.Session -> {
- val analyticsEvent = when (event.event) {
- AnalyticsAnOld.WcSessionEvent.Disconnect -> AnalyticsEventAnOld.WC_SESSION_DISCONNECTED
- AnalyticsAnOld.WcSessionEvent.Connect -> AnalyticsEventAnOld.WC_NEW_SESSION
- }
- handleAnalyticsEvent(
- event = analyticsEvent,
- params = mapOf(
- AnalyticsParamAnOld.WALLET_CONNECT_DAPP_URL.param to event.url,
- ).filterNotNull(),
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt
index 17e31d88c4..491ef8f683 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt
@@ -74,7 +74,7 @@ object TangemSdk {
is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig()
is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError(error.customMessage)
is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage)
- is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.throwable)
+ is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.cause)
is TangemSdkError.TooMuchBackupCards -> TangemSdkError.TooMuchBackupCards()
is TangemSdkError.BackupCardRequired -> TangemSdkError.BackupCardRequired()
is TangemSdkError.CertificateSignatureRequired -> TangemSdkError.CertificateSignatureRequired()
@@ -107,6 +107,8 @@ object TangemSdk {
is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode()
is TangemSdkError.BackupFailedIncompatibleBatch -> TangemSdkError.BackupFailedIncompatibleBatch()
+ is TangemSdkError.BiometricsUnavailable -> error
+ is TangemSdkError.BiometricsAuthenticationFailed -> error
}
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt
new file mode 100644
index 0000000000..04d6dbbb01
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt
@@ -0,0 +1,30 @@
+package com.tangem.tap.common.analytics.api
+
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
+
+/**
+[REDACTED_AUTHOR]
+ * The filter is used to separate events to send and determine which handler it can be processed with.
+ */
+interface AnalyticsEventFilter {
+ /**
+ * Recognizes the event and checks if the given filter can be applied to the event.
+ */
+ fun canBeAppliedTo(event: AnalyticsEvent): Boolean
+
+ /**
+ * An internal filter check that, on external or internal conditions, recognizes the possibility of
+ * sending an event.
+ */
+ fun canBeSent(event: AnalyticsEvent): Boolean
+
+ /**
+ * Performs a check to see if the event can be dispatched by a specific handler
+ */
+ fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean
+}
+
+interface AnalyticsFilterHolder {
+ fun addFilter(filter: AnalyticsEventFilter)
+ fun removeFilter(filter: AnalyticsEventFilter): Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt
index 08e1609e3c..344402f7f1 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt
@@ -1,88 +1,50 @@
package com.tangem.tap.common.analytics.api
-import com.shopify.buy3.Storefront
-import com.tangem.blockchain.common.BlockchainError
-import com.tangem.common.card.Card
-import com.tangem.common.core.TangemSdkError
-import com.tangem.tap.common.analytics.AnalyticsAnOld
-import com.tangem.tap.common.analytics.AnalyticsEventAnOld
-import com.tangem.tap.common.analytics.AnalyticsParamAnOld
+import android.app.Application
+import com.tangem.common.json.MoshiJsonConverter
+import com.tangem.domain.common.AnalyticsHandlersLogConfig
import com.tangem.tap.common.analytics.events.AnalyticsEvent
-import com.tangem.tap.common.analytics.events.AnalyticsParam
-import com.tangem.tap.common.extensions.filterNotNull
+import com.tangem.tap.domain.configurable.config.Config
/**
[REDACTED_AUTHOR]
*/
interface AnalyticsEventHandler {
- fun handleEvent(
- event: String,
- params: Map = emptyMap(),
- )
+ fun send(event: AnalyticsEvent)
+}
- fun handleAnalyticsEvent(
- event: AnalyticsEvent,
- card: Card? = null,
- blockchain: String? = null,
- ) {
- handleEvent(
- event = prepareEventString(event.category, event.event),
- params = prepareParams(card, blockchain, event.params),
- )
+interface AnalyticsHandler : AnalyticsEventHandler {
+ fun id(): String
+
+ fun send(event: String, params: Map = emptyMap())
+
+ override fun send(event: AnalyticsEvent) {
+ send(prepareEventString(event), event.params)
}
- @Deprecated("Migrate to AnalyticsEvent")
- fun handleAnalyticsEvent(
- event: AnalyticsEventAnOld,
- params: Map = emptyMap(),
- card: Card? = null,
- blockchain: String? = null,
- ) {
- handleEvent(event.event, prepareParams(card, blockchain, params))
- }
-
- fun prepareParams(
- card: Card? = null,
- blockchain: String? = null,
- params: Map = emptyMap(),
- ): Map = mapOf(
- AnalyticsParam.Firmware to card?.firmwareVersion?.stringValue,
- AnalyticsParam.BatchId to card?.batchId,
- AnalyticsParam.Blockchain to blockchain,
- ).filterNotNull() + params
-
- fun prepareEventString(category: String, event: String): String {
- return "[$category] $event"
- }
+ fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}"
}
interface ErrorEventHandler {
- fun handleErrorEvent(
+ fun send(
error: Throwable,
params: Map = emptyMap(),
)
}
-interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler
+interface AnalyticsHandlerHolder {
+ fun addHandler(name: String, handler: AnalyticsHandler)
+ fun removeHandler(name: String): AnalyticsHandler?
+}
-interface CardSdkErrorEventHandler {
- fun handleCardSdkErrorEvent(
- error: TangemSdkError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map = emptyMap(),
- card: Card? = null,
+interface AnalyticsHandlerBuilder {
+ fun build(data: Data): AnalyticsHandler?
+
+ data class Data(
+ val application: Application,
+ val config: Config,
+ val isDebug: Boolean,
+ val logConfig: AnalyticsHandlersLogConfig,
+ val jsonConverter: MoshiJsonConverter,
)
-}
-
-interface BlockchainSdkErrorEventHandler {
- fun handleBlockchainSdkErrorEvent(
- error: BlockchainError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map = mapOf(),
- card: Card? = null,
- )
-}
-
-interface ShopifyOrderEventHandler {
- fun handleShopifyOrderEvent(order: Storefront.Order)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/ParamsInterceptorApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/ParamsInterceptorApi.kt
new file mode 100644
index 0000000000..245ef32da5
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/api/ParamsInterceptorApi.kt
@@ -0,0 +1,17 @@
+package com.tangem.tap.common.analytics.api
+
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface ParamsInterceptor {
+ fun id(): String
+ fun canBeAppliedTo(event: AnalyticsEvent): Boolean
+ fun intercept(params: MutableMap)
+}
+
+interface ParamsInterceptorHolder {
+ fun addParamsInterceptor(interceptor: ParamsInterceptor)
+ fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor?
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/AnalyticsErrorConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/AnalyticsErrorConverter.kt
new file mode 100644
index 0000000000..b6fab47d07
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/AnalyticsErrorConverter.kt
@@ -0,0 +1,70 @@
+package com.tangem.tap.common.analytics.converters
+
+import com.tangem.blockchain.common.BlockchainSdkError
+import com.tangem.common.Converter
+import com.tangem.common.core.TangemSdkError
+import com.tangem.tap.common.analytics.events.AnalyticsParam
+import com.tangem.tap.features.demo.DemoTransactionSender
+
+/**
+[REDACTED_AUTHOR]
+ */
+class AnalyticsErrorConverter : Converter> {
+ fun canBeHandled(error: Throwable): Boolean {
+ return when (error) {
+ is TangemSdkError.UserCancelled -> false
+ is BlockchainSdkError -> !error.customMessage.contains(DemoTransactionSender.ID)
+ else -> true
+ }
+ }
+
+ override fun convert(value: Throwable): Map {
+ return when (value) {
+ is BlockchainSdkError -> BlockchainSdkErrorConverter(CardSdkErrorConverter()).convert(value)
+ is TangemSdkError -> CardSdkErrorConverter().convert(value)
+ else -> ThrowableErrorConverter().convert(value)
+ }
+ }
+}
+
+private class ThrowableErrorConverter : Converter> {
+ override fun convert(value: Throwable): Map {
+ val unknown = "unknown"
+ return mapOf(
+ AnalyticsParam.ErrorKey to AnalyticsParam.Error.App.value,
+ AnalyticsParam.ErrorDescription to (value.message ?: value.cause?.message ?: unknown),
+ )
+ }
+}
+
+class CardSdkErrorConverter : Converter> {
+
+ override fun convert(value: TangemSdkError): Map {
+ if (value is TangemSdkError.UserCancelled) return emptyMap()
+
+ return mapOf(
+ AnalyticsParam.ErrorKey to AnalyticsParam.Error.CardSdk.value,
+ AnalyticsParam.ErrorCode to value.code.toString(),
+ AnalyticsParam.ErrorDescription to value.toString(),
+ )
+ }
+}
+
+private class BlockchainSdkErrorConverter(
+ private val cardSdkErrorConverter: CardSdkErrorConverter,
+) : Converter> {
+
+ override fun convert(value: BlockchainSdkError): Map {
+ if (value.customMessage.contains(DemoTransactionSender.ID)) return emptyMap()
+
+ if (value is BlockchainSdkError.WrappedTangemError) {
+ return (value.tangemError as? TangemSdkError)?.let { cardSdkErrorConverter.convert(it) } ?: emptyMap()
+ }
+
+ return mapOf(
+ AnalyticsParam.ErrorKey to AnalyticsParam.Error.BlockchainSdk.value,
+ AnalyticsParam.ErrorCode to value.code.toString(),
+ AnalyticsParam.ErrorDescription to "${value.javaClass.simpleName}: ${value.customMessage}",
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt
new file mode 100644
index 0000000000..52fa450434
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt
@@ -0,0 +1,78 @@
+package com.tangem.tap.common.analytics.converters
+
+import com.tangem.common.Converter
+import com.tangem.common.extensions.isZero
+import com.tangem.domain.common.ScanResponse
+import com.tangem.tap.common.analytics.events.AnalyticsParam
+import com.tangem.tap.common.analytics.events.Basic
+import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
+import com.tangem.tap.domain.extensions.isMultiwalletAllowed
+import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
+import com.tangem.tap.features.wallet.redux.ProgressState
+import com.tangem.tap.features.wallet.redux.WalletData
+import com.tangem.tap.features.wallet.redux.WalletState
+import com.tangem.tap.features.wallet.redux.reducers.calculateTotalCryptoAmount
+
+/**
+[REDACTED_AUTHOR]
+ */
+class BasicSignInEventConverter(
+ private val scanResponse: ScanResponse,
+) : Converter {
+
+ override fun convert(value: WalletState): Basic.SignedIn? {
+ if (!statesIsReadyToCreateEvent(scanResponse, value)) return null
+ val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
+
+ return Basic.SignedIn(
+ state = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
+ currency = cardCurrency,
+ batch = scanResponse.card.batchId,
+ ).apply {
+ filterData = UserWalletIdBuilder.scanResponse(scanResponse)
+ .build()
+ ?.stringValue
+ }
+ }
+}
+
+class BasicTopUpEventConverter(
+ private val scanResponse: ScanResponse,
+) : Converter {
+
+ override fun convert(value: WalletState): Basic.ToppedUp? {
+ if (!statesIsReadyToCreateEvent(scanResponse, value)) return null
+ val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
+
+ val data = BasicTopUpFilter.Data(
+ walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()?.stringValue ?: "",
+ cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
+ )
+
+ return Basic.ToppedUp(cardCurrency).apply { filterData = data }
+ }
+}
+
+private fun AnalyticsParam.CardBalanceState.Companion.from(walletsData: List): AnalyticsParam.CardBalanceState {
+ val totalCryptoAmount = walletsData.calculateTotalCryptoAmount()
+ return when {
+ totalCryptoAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty
+ else -> AnalyticsParam.CardBalanceState.Full
+ }
+}
+
+private fun statesIsReadyToCreateEvent(scanResponse: ScanResponse, state: WalletState): Boolean {
+ if (scanResponse.card.isMultiwalletAllowed && state.missingDerivations.isNotEmpty()) return false
+ if (state.walletsDataFromStores.isEmpty()) return false
+
+ val totalBalanceState = state.totalBalance?.state ?: return false
+ if (totalBalanceState == ProgressState.Loading || totalBalanceState == ProgressState.Refreshing) return false
+
+ val balancesCount = state.walletsDataFromStores.map {
+ if (it.currencyData.amount == null) 0 else 1
+ }.reduce { acc, i -> acc + i }
+
+ if (balancesCount != state.walletsStores.size) return false
+
+ return true
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt
new file mode 100644
index 0000000000..e64b751f4f
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt
@@ -0,0 +1,31 @@
+package com.tangem.tap.common.analytics.converters
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.common.Converter
+import com.tangem.domain.common.SaltPayWorkaround
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
+import com.tangem.tap.common.analytics.events.AnalyticsParam
+import com.tangem.tap.domain.extensions.isMultiwalletAllowed
+
+/**
+[REDACTED_AUTHOR]
+ */
+class ParamCardCurrencyConverter : Converter {
+
+ override fun convert(value: ScanResponse): AnalyticsParam.CardCurrency? {
+ if (value.card.isMultiwalletAllowed) return AnalyticsParam.CardCurrency.MultiCurrency
+
+ val type = when {
+ value.isTangemNote() -> {
+ value.card.getTangemNoteBlockchain()?.let { AnalyticsParam.CurrencyType.Blockchain(it) }
+ }
+ value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
+ value.isSaltPay() -> AnalyticsParam.CurrencyType.Token(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay))
+ value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
+ else -> null
+ } ?: return null
+
+ return AnalyticsParam.CardCurrency.SingleCurrency(type)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt
deleted file mode 100644
index 5229df8174..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.tangem.tap.common.analytics.converters
-
-import com.tangem.blockchain.common.BlockchainError
-import com.tangem.blockchain.common.BlockchainSdkError
-import com.tangem.common.Converter
-import com.tangem.common.core.TangemSdkError
-import com.tangem.tap.common.analytics.AnalyticsAnOld
-import com.tangem.tap.common.analytics.AnalyticsParamAnOld
-import com.tangem.tap.common.analytics.TangemSdk
-import com.tangem.tap.features.demo.DemoTransactionSender
-
-/**
-[REDACTED_AUTHOR]
- */
-internal data class ErrorEvent(
- val throwable: Throwable,
- val params: Map,
-)
-
-internal class CardSdkErrorConverter : Converter {
-
- override fun convert(value: Model): ErrorEvent? {
- if (value.error is TangemSdkError.UserCancelled) return null
-
- val mutableParams = value.params.convertToParamValue().toMutableMap().apply {
- putAll(value.cardParams)
- }
- mutableParams[AnalyticsParamAnOld.ACTION.param] = value.action.key
- mutableParams[AnalyticsParamAnOld.ERROR_CODE.param] = value.error.code.toString()
- mutableParams[AnalyticsParamAnOld.ERROR_DESCRIPTION.param] = value.error.javaClass.simpleName
- mutableParams[AnalyticsParamAnOld.ERROR_KEY.param] = "TangemSdkError"
-
- return ErrorEvent(TangemSdk.map(value.error), mutableParams)
- }
-
- internal data class Model(
- val error: TangemSdkError,
- val action: AnalyticsAnOld.ActionToLog,
- val params: Map,
- val cardParams: Map,
- )
-}
-
-internal class BlockchainSdkErrorConverter(
- private val cardSdkErrorConverter: CardSdkErrorConverter,
-) : Converter {
-
- override fun convert(value: Model): ErrorEvent? {
- val error = (value.error as? BlockchainSdkError) ?: return null
-
- if (error is BlockchainSdkError.WrappedTangemError) {
- val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return null
-
- val cardSdkModel = CardSdkErrorConverter.Model(
- error = tangemSdkError,
- action = value.action,
- params = value.params,
- cardParams = value.cardParams,
- )
- return cardSdkErrorConverter.convert(cardSdkModel)
- }
-
- if (error.customMessage.contains(DemoTransactionSender.ID)) return null
-
- val mutableParams = value.params.convertToParamValue().toMutableMap()
- mutableParams[AnalyticsParamAnOld.ACTION.param] = value.action.key
- mutableParams[AnalyticsParamAnOld.ERROR_CODE.param] = error.code.toString()
- mutableParams[AnalyticsParamAnOld.ERROR_DESCRIPTION.param] = "${error.javaClass.simpleName}: ${error.customMessage}"
- mutableParams[AnalyticsParamAnOld.ERROR_KEY.param] = "BlockchainSdkError"
-
- return ErrorEvent(value.error, mutableParams)
- }
-
- data class Model(
- val error: BlockchainError,
- val action: AnalyticsAnOld.ActionToLog,
- val params: Map,
- val cardParams: Map,
- )
-}
-
-internal fun Map.convertToParamValue(): Map {
- return mutableMapOf().also { newMap ->
- this.forEach {
- newMap[it.key.param] = it.value
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt
new file mode 100644
index 0000000000..ddbd1683bd
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt
@@ -0,0 +1,27 @@
+package com.tangem.tap.common.analytics.converters
+
+import com.shopify.buy3.Storefront
+import com.tangem.common.Converter
+import com.tangem.tap.common.analytics.events.Shop
+import com.tangem.tap.common.shop.data.ProductType
+
+/**
+[REDACTED_AUTHOR]
+ */
+class ShopOrderToEventConverter : Converter, Shop.Purchased> {
+
+ override fun convert(value: Pair): Shop.Purchased {
+ val order = value.first
+ val productType = value.second
+
+ val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: productType.sku
+ val count = when (productType) {
+ ProductType.WALLET_2_CARDS -> "2"
+ ProductType.WALLET_3_CARDS -> "3"
+ }
+ val amount = "${order.totalPriceV2.amount} ${order.totalPriceV2.currencyCode.name}"
+ val code = (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
+
+ return Shop.Purchased(sku, count, amount, code)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt
index 0451fdd68c..b2db44fe52 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt
@@ -6,5 +6,7 @@ package com.tangem.tap.common.analytics.events
sealed class AnalyticsEvent(
val category: String,
val event: String,
- val params: Map = mapOf(),
+ var params: Map = mapOf(),
+ val error: Throwable? = null,
+ var filterData: Any? = null,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt
index 8c69239429..bad36d3f0d 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt
@@ -1,22 +1,27 @@
package com.tangem.tap.common.analytics.events
+import com.tangem.tap.features.details.redux.SecurityOption
+
sealed class AnalyticsParam {
sealed class CurrencyType(val value: String) {
class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol)
class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency)
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
+ class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.symbol)
+ class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
}
- // Multicurrency or CurrencyType
+ // MultiCurrency or CurrencyType
sealed class CardCurrency(val value: String) {
object MultiCurrency : CardCurrency("Multicurrency")
class SingleCurrency(type: CurrencyType) : CardCurrency(type.value)
}
- sealed class CardState(val value: String) {
- object Empty : CardState("Empty")
- object Full : CardState("Full")
+ sealed class CardBalanceState(val value: String) {
+ object Empty : CardBalanceState("Empty")
+ object Full : CardBalanceState("Full")
+ companion object
}
sealed class RateApp(val value: String) {
@@ -39,20 +44,26 @@ sealed class AnalyticsParam {
object AccessCode : SecurityMode("Access Code")
object Passcode : SecurityMode("Passcode")
object LongTap : SecurityMode("Long Tap")
+
+ companion object {
+ fun from(option: SecurityOption): SecurityMode = when (option) {
+ SecurityOption.AccessCode -> AccessCode
+ SecurityOption.PassCode -> Passcode
+ SecurityOption.LongTap -> LongTap
+ }
+ }
}
- sealed class SocialNetwork(val value: String) {
- object Facebook : SocialNetwork("Facebook")
- object Instagram : SocialNetwork("Instagram")
- object Youtube : SocialNetwork("Youtube")
- object Twitter : SocialNetwork("Twitter")
- object LinkedIn : SocialNetwork("LinkedIn")
- object GitHub : SocialNetwork("GitHub")
+ sealed class Error(val value: String) {
+ object App : Error("App Error")
+ object CardSdk : Error("Card Sdk Error")
+ object BlockchainSdk : Error("Blockchain Sdk Error")
}
companion object Key {
- const val Blockchain = "blockchain"
- const val BatchId = "batch_id"
- const val Firmware = "firmware"
+ const val BatchId = "Batch"
+ const val ErrorDescription = "Error Description"
+ const val ErrorCode = "Error Code"
+ const val ErrorKey = "Error Key"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt
index bb37145672..80afe6d3f8 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt
@@ -6,10 +6,11 @@ package com.tangem.tap.common.analytics.events
sealed class Basic(
event: String,
params: Map = mapOf(),
-) : AnalyticsEvent("Basic", event, params) {
+ error: Throwable? = null,
+) : AnalyticsEvent("Basic", event, params, error) {
- sealed class SignedIn(
- state: AnalyticsParam.CardState,
+ class SignedIn(
+ state: AnalyticsParam.CardBalanceState,
currency: AnalyticsParam.CardCurrency,
batch: String,
) : Basic(
@@ -17,7 +18,7 @@ sealed class Basic(
params = mapOf(
"State" to state.value,
"Currency" to currency.value,
- "Batch" to batch,
+ AnalyticsParam.BatchId to batch,
),
)
@@ -25,4 +26,9 @@ sealed class Basic(
event = "Topped up",
params = mapOf("Currency" to currency.value),
)
+
+ class ScanError(error: Throwable) : Basic(
+ event = "Scan",
+ error = error,
+ )
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt
index c6825718b7..a42cc3568e 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt
@@ -14,12 +14,17 @@ sealed class MainScreen(
class CardWasScanned : MainScreen("Card Was Scanned")
class ButtonMyWallets : MainScreen("Button - My Wallets")
+ class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
+ event = "Enable Biometric",
+ params = mapOf("State" to state.value),
+ )
+
class MainCurrencyChanged(currencyType: AnalyticsParam.CurrencyType) : MainScreen(
event = "Main Currency Changed",
params = mapOf("Currency Type" to currencyType.value),
)
- sealed class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
+ class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
event = "Notice - Rate The App Button Tapped",
params = mapOf("Result" to result.value),
)
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt
index e5e3fdb96a..f944f58d24 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt
@@ -1,5 +1,9 @@
package com.tangem.tap.common.analytics.events
+import com.tangem.domain.common.extensions.toNetworkId
+import com.tangem.domain.features.addCustomToken.CustomCurrency
+import com.tangem.tap.common.extensions.filterNotNull
+
/**
[REDACTED_AUTHOR]
*/
@@ -10,7 +14,18 @@ sealed class ManageTokens(
class ScreenOpened : ManageTokens("Manage Tokens Screen Opened")
class TokenSearched : ManageTokens("Token Searched")
- class TokenSwitcherChanged : ManageTokens("Token Switcher Changed")
+
+ class TokenSwitcherChanged(
+ type: AnalyticsParam.CurrencyType,
+ state: AnalyticsParam.OnOffState,
+ ) : ManageTokens(
+ "Token Switcher Changed",
+ params = mapOf(
+ "Token" to type.value,
+ "State" to state.value,
+ ),
+ )
+
class ButtonSaveChanges : ManageTokens("Button - Save Changes")
class ButtonCustomToken : ManageTokens("Button - Custom Token")
@@ -21,13 +36,26 @@ sealed class ManageTokens(
class ScreenOpened : ManageTokens("Custom Token Screen Opened")
- class TokenWasAdded(symbol: String, network: String, address: String) : ManageTokens(
+ class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens(
event = "Custom Token Was Added",
- params = mapOf(
- "Symbol" to symbol,
- "Network" to network,
- "Address" to address,
- ),
- )
+ params = convertToParam(customCurrency),
+ ) {
+ companion object {
+ private fun convertToParam(customCurrency: CustomCurrency): Map = with(customCurrency) {
+ return when (this) {
+ is CustomCurrency.CustomBlockchain -> mapOf(
+ "Token" to network.currency,
+ "Derivation Path" to derivationPath?.rawPath,
+ ).filterNotNull()
+ is CustomCurrency.CustomToken -> mapOf(
+ "Token" to token.symbol,
+ "Derivation Path" to derivationPath?.rawPath,
+ "Network Id" to network.toNetworkId(),
+ "Contract Address" to token.contractAddress,
+ ).filterNotNull()
+ }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt
new file mode 100644
index 0000000000..0fd08ddd88
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt
@@ -0,0 +1,18 @@
+package com.tangem.tap.common.analytics.events
+
+sealed class MyWallets(
+ event: String,
+ params: Map = mapOf(),
+) : AnalyticsEvent("My Wallets", event, params) {
+
+ object MyWalletsScreenOpened : MyWallets(event = "My Wallets Screen Opened")
+ object CardWasScanned : MyWallets(event = "Card Was Scanned")
+ object WalletUnlockTapped : MyWallets(event = "Wallet Unlock Tapped")
+
+ object Button {
+ object ScanNewCard : MyWallets(event = "Button - Scan New Card")
+ object UnlockWithBiometrics : MyWallets(event = "Button - Unlock all with Face ID")
+ object EditWalletTapped : MyWallets(event = "Button - Edit Wallet Tapped")
+ object DeleteWalletTapped : MyWallets(event = "Button - Delete Wallet Tapped")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt
index b65408b7d5..92f55074e9 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt
@@ -34,9 +34,9 @@ sealed class Onboarding(
class AccessCodeEntered : Backup("Access Code Entered")
class AccessCodeReEntered : Backup("Access Code Re-entered")
- class Finished(cardsCount: String) : Backup(
+ class Finished(cardsCount: Int) : Backup(
event = "Backup Finished",
- params = mapOf("Cards count" to cardsCount),
+ params = mapOf("Cards count" to "$cardsCount"),
)
}
@@ -65,12 +65,17 @@ sealed class Onboarding(
class SetupFinished : Twins("Twin Setup Finished")
}
- class PinCodeSet: Onboarding("Onboarding", "PIN code set")
- class ButtonConnect: Onboarding("Onboarding", "Button - Connect")
- class KYCStarted: Onboarding("Onboarding", "KYC started")
- class KYCInProgress: Onboarding("Onboarding", "KYC in progress")
- class KYCRejected: Onboarding("Onboarding", "KYC rejected")
- class ClaimScreenOpened: Onboarding("Onboarding", "Claim screen opened")
- class ButtonClaim: Onboarding("Onboarding", "Button - Claim")
- class ClaimWasSuccessfully: Onboarding("Onboarding", "Claim was successfully")
+ class PinCodeSet : Onboarding("Onboarding", "PIN code set")
+ class ButtonConnect : Onboarding("Onboarding", "Button - Connect")
+ class KYCStarted : Onboarding("Onboarding", "KYC started")
+ class KYCInProgress : Onboarding("Onboarding", "KYC in progress")
+ class KYCRejected : Onboarding("Onboarding", "KYC rejected")
+ class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened")
+ class ButtonClaim : Onboarding("Onboarding", "Button - Claim")
+ class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully")
+ class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
+ category = "Onboarding / Biometric",
+ event = "Enable Biometric",
+ params = mapOf("State" to state.value),
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt
index 56893d2f31..728662bdb9 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt
@@ -1,5 +1,7 @@
package com.tangem.tap.common.analytics.events
+import com.tangem.tap.features.details.ui.details.SocialNetwork
+
/**
[REDACTED_AUTHOR]
*/
@@ -7,7 +9,8 @@ sealed class Settings(
category: String = "Settings",
event: String,
params: Map = mapOf(),
-) : AnalyticsEvent(category, event, params) {
+ error: Throwable? = null,
+) : AnalyticsEvent(category, event, params, error) {
class ScreenOpened : Settings(event = "Settings Screen Opened")
class ButtonChat : Settings(event = "Button - Chat")
@@ -18,29 +21,35 @@ sealed class Settings(
class ButtonAppSettings : Settings(event = "Button - App Settings")
class ButtonCreateBackup : Settings(event = "Button - Create Backup")
- sealed class ButtonSocialNetwork(network: AnalyticsParam.SocialNetwork) : Settings(
+ class ButtonSocialNetwork(network: SocialNetwork) : Settings(
event = "Button - Social Network",
- params = mapOf("Network" to network.value),
+ params = mapOf("Network" to network.id),
)
sealed class CardSettings(
event: String,
params: Map = mapOf(),
- ) : Settings("Settings / Card Settings", event, params) {
+ error: Throwable? = null,
+ ) : Settings("Settings / Card Settings", event, params, error) {
class ButtonFactoryReset : CardSettings("Button - Factory Reset")
- class FactoryResetFinished : CardSettings("Factory Reset Finished")
+ class FactoryResetFinished(error: Throwable? = null) : CardSettings(
+ event = "Factory Reset Finished",
+ error = error,
+ )
+
class UserCodeChanged : CardSettings("User Code Changed")
class ButtonChangeSecurityMode : CardSettings("Button - Change Security Mode")
- sealed class ButtonChangeUserCode(type: AnalyticsParam.UserCode) : CardSettings(
+ class ButtonChangeUserCode(type: AnalyticsParam.UserCode) : CardSettings(
event = "Button - Change User Code",
params = mapOf("Type" to type.value),
)
- sealed class SecurityModeChanged(mode: AnalyticsParam.SecurityMode) : CardSettings(
+ class SecurityModeChanged(mode: AnalyticsParam.SecurityMode, error: Throwable? = null) : CardSettings(
event = "Security Mode Changed",
params = mapOf("Mode" to mode.value),
+ error = error,
)
}
@@ -49,16 +58,16 @@ sealed class Settings(
params: Map = mapOf(),
) : Settings("Settings / App Settings", event, params) {
- sealed class FaceIDSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
- event = "Face ID Switcher Changed",
+ class SaveWalletSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
+ event = "Save Wallet Switcher Changed",
params = mapOf("State" to state.value),
)
- sealed class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
+ class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
event = "Save Access Code Switcher Changed",
params = mapOf("State" to state.value),
)
- class ButtonEnableBiometricAuthentication : AppSettings("Button - Enable Biometric Authentication")
+ object ButtonEnableBiometricAuthentication : AppSettings("Button - Enable Biometric Authentication")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt
index c09bf89ab3..9634f2929b 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt
@@ -1,5 +1,7 @@
package com.tangem.tap.common.analytics.events
+import com.tangem.tap.common.extensions.filterNotNull
+
/**
[REDACTED_AUTHOR]
*/
@@ -10,9 +12,14 @@ sealed class Shop(
class ScreenOpened : IntroductionProcess("Shop Screen Opened")
- class Purchased(productSku: String) : Shop(
+ class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop(
event = "Purchased",
- params = mapOf("SKU" to productSku),
+ params = mapOf(
+ "SKU" to sku,
+ "Count" to count,
+ "Amount" to amount,
+ "Coupon Code" to couponCode,
+ ).filterNotNull(),
)
class Redirected(partnerName: String) : Shop(
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt
index afe5da529a..7445e31f33 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt
@@ -1,5 +1,7 @@
package com.tangem.tap.common.analytics.events
+import com.tangem.tap.common.analytics.events.AnalyticsParam.CurrencyType
+
/**
[REDACTED_AUTHOR]
*/
@@ -7,15 +9,41 @@ sealed class Token(
category: String,
event: String,
params: Map = mapOf(),
-) : AnalyticsEvent(category, event, params) {
+ error: Throwable? = null,
+) : AnalyticsEvent(category, event, params, error) {
class Refreshed : Token("Token", "Refreshed")
- class ButtonRemoveToken : Token("Token", "Button - Remove Token")
class ButtonExplore : Token("Token", "Button - Explore")
- class ButtonBuy(token: String) : Token("Token", "Button - Buy", mapOf("Token" to token))
- class ButtonSell(token: String) : Token("Token", "Button - Sell", mapOf("Token" to token))
- class ButtonExchange(token: String) : Token("Token", "Button - Exchange", mapOf("Token" to token))
- class ButtonSend(token: String) : Token("Token", "Button - Send", mapOf("Token" to token))
+
+ class ButtonRemoveToken(type: CurrencyType) : Token(
+ "Token",
+ "Button - Remove Token",
+ params = mapOf("Token" to type.value),
+ )
+
+ class ButtonBuy(type: CurrencyType) : Token(
+ category = "Token",
+ event = "Button - Buy",
+ params = mapOf("Token" to type.value),
+ )
+
+ class ButtonSell(type: CurrencyType) : Token(
+ category = "Token",
+ event = "Button - Sell",
+ params = mapOf("Token" to type.value),
+ )
+
+ class ButtonExchange(type: CurrencyType) : Token(
+ category = "Token",
+ event = "Button - Exchange",
+ params = mapOf("Token" to type.value),
+ )
+
+ class ButtonSend(type: CurrencyType) : Token(
+ category = "Token",
+ event = "Button - Send",
+ params = mapOf("Token" to type.value),
+ )
sealed class Recieve(
event: String,
@@ -30,16 +58,18 @@ sealed class Token(
sealed class Send(
event: String,
params: Map = mapOf(),
- ) : Token("Token / Send", event, params) {
+ error: Throwable? = null,
+ ) : Token("Token / Send", event, params, error) {
class ScreenOpened : Send("Send Screen Opened")
class ButtonPaste : Send("Button - Paste")
class ButtonQRCode : Send("Button - QR Code")
class ButtonSwapCurrency : Send("Button - Swap Currency")
- class TransactionSent(token: AnalyticsParam.CurrencyType) : Send(
+ class TransactionSent(type: CurrencyType, error: Throwable? = null) : Send(
event = "Transaction Sent",
- params = mapOf("Token" to token.value),
+ params = mapOf("Token" to type.value),
+ error = error,
)
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt
index 624145c63d..1ff587b2aa 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt
@@ -6,9 +6,13 @@ package com.tangem.tap.common.analytics.events
sealed class WalletConnect(
event: String,
params: Map = mapOf(),
-) : AnalyticsEvent("Wallet Connect", event, params) {
+ error: Throwable? = null,
+) : AnalyticsEvent("Wallet Connect", event, params, error) {
class NewSessionEstablished : WalletConnect("New Session Established")
class SessionDisconnected : WalletConnect("Session Disconnected")
class RequestSigned : WalletConnect("Request Signed")
+
+ class SignError(error: Throwable) : WalletConnect("Sign", error = error)
+ class TransactionError(error: Throwable) : WalletConnect("Transaction", error = error)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicSignInFilter.kt b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicSignInFilter.kt
new file mode 100644
index 0000000000..a582ea34f1
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicSignInFilter.kt
@@ -0,0 +1,27 @@
+package com.tangem.tap.common.analytics.filters
+
+import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
+import com.tangem.tap.common.analytics.api.AnalyticsHandler
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
+import com.tangem.tap.common.analytics.events.Basic
+
+/**
+[REDACTED_AUTHOR]
+ */
+class BasicSignInFilter : AnalyticsEventFilter {
+
+ private val alreadySignedIn = mutableSetOf()
+
+ override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Basic.SignedIn
+
+ override fun canBeSent(event: AnalyticsEvent): Boolean {
+ val userWalletId = event.filterData as? String ?: return false
+
+ val canBeSent = !alreadySignedIn.contains(userWalletId)
+ alreadySignedIn.add(userWalletId)
+
+ return canBeSent
+ }
+
+ override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean = true
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicTopUpFilter.kt b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicTopUpFilter.kt
new file mode 100644
index 0000000000..be780ccd2d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicTopUpFilter.kt
@@ -0,0 +1,50 @@
+package com.tangem.tap.common.analytics.filters
+
+import com.tangem.common.extensions.guard
+import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
+import com.tangem.tap.common.analytics.api.AnalyticsHandler
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
+import com.tangem.tap.common.analytics.events.AnalyticsParam
+import com.tangem.tap.common.analytics.events.Basic
+import com.tangem.tap.persistence.ToppedUpWalletStorage
+
+/**
+[REDACTED_AUTHOR]
+ */
+class BasicTopUpFilter(
+ private val topupWalletStorage: ToppedUpWalletStorage,
+) : AnalyticsEventFilter {
+
+ override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Basic.ToppedUp
+
+ override fun canBeSent(event: AnalyticsEvent): Boolean {
+ val data = event.filterData as? Data ?: return false
+
+ val walletInfo = topupWalletStorage.restore(data.walletId).guard {
+ val newWalletInfo = Data(
+ walletId = data.walletId,
+ cardBalanceState = data.cardBalanceState,
+ )
+ topupWalletStorage.save(newWalletInfo)
+ return false
+ }
+
+ if (walletInfo.isToppedUp) return false
+
+ return if (!walletInfo.isToppedUp && data.isToppedUp) {
+ topupWalletStorage.save(walletInfo.copy(cardBalanceState = AnalyticsParam.CardBalanceState.Full))
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean = true
+
+ data class Data(
+ val walletId: String,
+ val cardBalanceState: AnalyticsParam.CardBalanceState,
+ ) {
+ val isToppedUp: Boolean = cardBalanceState == AnalyticsParam.CardBalanceState.Full
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/filters/ShopPurchasedEventFilter.kt b/app/src/main/java/com/tangem/tap/common/analytics/filters/ShopPurchasedEventFilter.kt
new file mode 100644
index 0000000000..dffa123240
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/filters/ShopPurchasedEventFilter.kt
@@ -0,0 +1,25 @@
+package com.tangem.tap.common.analytics.filters
+
+import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
+import com.tangem.tap.common.analytics.api.AnalyticsHandler
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
+import com.tangem.tap.common.analytics.events.Shop
+import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
+import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
+
+/**
+[REDACTED_AUTHOR]
+ */
+class ShopPurchasedEventFilter : AnalyticsEventFilter {
+
+ override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Shop.Purchased
+
+ override fun canBeSent(event: AnalyticsEvent): Boolean = true
+
+ override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
+ return when (handler) {
+ is AppsFlyerAnalyticsHandler, is FirebaseAnalyticsHandler -> true
+ else -> false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt
index 27132a1f61..c76fe21d38 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt
@@ -1,12 +1,27 @@
package com.tangem.tap.common.analytics.handlers.amplitude
-import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
+import com.tangem.tap.common.analytics.api.AnalyticsHandler
+import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
class AmplitudeAnalyticsHandler(
private val client: AmplitudeAnalyticsClient,
-) : AnalyticsEventHandler {
+) : AnalyticsHandler {
- override fun handleEvent(event: String, params: Map) {
+ override fun id(): String = ID
+
+ override fun send(event: String, params: Map) {
client.logEvent(event, params)
}
+
+ companion object {
+ const val ID = "Amplitude"
+ }
+
+ class Builder : AnalyticsHandlerBuilder {
+ override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
+ !data.isDebug -> AmplitudeClient(data.application, data.config.amplitudeApiKey)
+ data.isDebug && data.logConfig.amplitude -> AmplitudeLogClient(data.jsonConverter)
+ else -> null
+ }?.let { AmplitudeAnalyticsHandler(it) }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt
index b800259b5f..f60d2edfcf 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt
@@ -1,14 +1,17 @@
package com.tangem.tap.common.analytics.handlers.amplitude
+import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class AmplitudeLogClient(
- private val logger: AnalyticsEventsLogger,
+ jsonConverter: MoshiJsonConverter,
) : AmplitudeAnalyticsClient {
+ private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(AmplitudeAnalyticsHandler.ID, jsonConverter)
+
override fun logEvent(event: String, params: Map) {
logger.logEvent(event, params)
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt
index 0acfb1dc2f..c3968d9d72 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt
@@ -1,47 +1,27 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
-import com.appsflyer.AFInAppEventParameterName
-import com.appsflyer.AFInAppEventType
-import com.shopify.buy3.Storefront
-import com.tangem.common.Converter
-import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
-import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler
+import com.tangem.tap.common.analytics.api.AnalyticsHandler
+import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
-) : AnalyticsEventHandler, ShopifyOrderEventHandler {
+) : AnalyticsHandler {
- override fun handleEvent(event: String, params: Map) {
+ override fun id(): String = ID
+
+ override fun send(event: String, params: Map) {
client.logEvent(event, params)
}
- override fun handleShopifyOrderEvent(order: Storefront.Order) {
- handleEvent(ORDER_EVENT, OrderToParamsConverter().convert(order))
- }
-
companion object {
- const val ORDER_EVENT = AFInAppEventType.PURCHASE
+ const val ID = "AppsFlyer"
}
-}
-private class OrderToParamsConverter : Converter> {
-
- override fun convert(value: Storefront.Order): Map {
- val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
-
- val discountCode =
- (value.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
-
- val discountParams = if (discountCode != null) {
- mapOf(AFInAppEventParameterName.COUPON_CODE to discountCode)
- } else {
- mapOf()
- }
-
- return mapOf(
- AFInAppEventParameterName.CONTENT_ID to sku,
- AFInAppEventParameterName.REVENUE to value.totalPriceV2.amount,
- AFInAppEventParameterName.CURRENCY to value.currencyCode.name,
- ) + discountParams
+ class Builder : AnalyticsHandlerBuilder {
+ override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
+ !data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerDevKey)
+ data.isDebug && data.logConfig.appsFlyer -> AppsFlyerLogClient(data.jsonConverter)
+ else -> null
+ }?.let { AppsFlyerAnalyticsHandler(it) }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt
index 0d3737a161..8e87061b2c 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt
@@ -1,14 +1,17 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
+import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class AppsFlyerLogClient(
- private val logger: AnalyticsEventsLogger,
+ jsonConverter: MoshiJsonConverter,
) : AppsFlyerAnalyticsClient {
+ private val logger = AnalyticsEventsLogger(AppsFlyerAnalyticsHandler.ID, jsonConverter)
+
override fun logEvent(event: String, params: Map) {
logger.logEvent(event, params)
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt
index 3ab6326670..109e61d0a5 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt
@@ -1,81 +1,50 @@
package com.tangem.tap.common.analytics.handlers.firebase
-import com.google.firebase.analytics.FirebaseAnalytics
-import com.shopify.buy3.Storefront
-import com.tangem.blockchain.common.BlockchainError
-import com.tangem.common.Converter
-import com.tangem.common.card.Card
-import com.tangem.common.core.TangemSdkError
-import com.tangem.tap.common.analytics.AnalyticsAnOld
-import com.tangem.tap.common.analytics.AnalyticsParamAnOld
-import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
+import com.tangem.tap.common.analytics.api.AnalyticsHandler
+import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.api.ErrorEventHandler
-import com.tangem.tap.common.analytics.api.SdkErrorEventHandler
-import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler
-import com.tangem.tap.common.analytics.converters.BlockchainSdkErrorConverter
-import com.tangem.tap.common.analytics.converters.CardSdkErrorConverter
+import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
class FirebaseAnalyticsHandler(
private val client: FirebaseAnalyticsClient,
-) : AnalyticsEventHandler, ErrorEventHandler, SdkErrorEventHandler, ShopifyOrderEventHandler {
+) : AnalyticsHandler, ErrorEventHandler {
- override fun handleEvent(event: String, params: Map) {
+ override fun id(): String = ID
+
+ override fun send(event: String, params: Map) {
client.logEvent(event, params)
}
- override fun handleErrorEvent(error: Throwable, params: Map) {
+ override fun send(event: AnalyticsEvent) {
+ when (event.error) {
+ null -> super.send(event)
+ else -> {
+ val errorConverter = AnalyticsErrorConverter()
+ if (!errorConverter.canBeHandled(event.error)) return
+
+ val errorParams = errorConverter.convert(event.error).toMutableMap()
+ errorParams["Category"] = event.category
+ errorParams["Event"] = event.event
+ errorParams.putAll(event.params)
+ send(event.error, errorParams)
+ }
+ }
+ }
+
+ override fun send(error: Throwable, params: Map) {
client.logErrorEvent(error, params)
}
- override fun handleCardSdkErrorEvent(
- error: TangemSdkError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map,
- card: Card?,
- ) {
- val model = CardSdkErrorConverter.Model(error, action, params, prepareParams(card))
- val converter = CardSdkErrorConverter()
- converter.convert(model)?.let {
- handleErrorEvent(it.throwable, it.params)
- }
+ companion object {
+ const val ID = "Firebase"
}
- override fun handleBlockchainSdkErrorEvent(
- error: BlockchainError,
- action: AnalyticsAnOld.ActionToLog,
- params: Map,
- card: Card?,
- ) {
-
- val model = BlockchainSdkErrorConverter.Model(error, action, params, prepareParams(card))
- val converter = BlockchainSdkErrorConverter(CardSdkErrorConverter())
- converter.convert(model)?.let {
- handleErrorEvent(it.throwable, it.params)
- }
- }
-
- override fun handleShopifyOrderEvent(order: Storefront.Order) {
- handleEvent(FirebaseClient.ORDER_EVENT, OrderToParamsConverter().convert(order))
- }
-}
-
-private class OrderToParamsConverter : Converter> {
- override fun convert(value: Storefront.Order): Map {
- val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
-
- val discountCode =
- (value.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
-
- val discountParams = if (discountCode != null) {
- mapOf(FirebaseAnalytics.Param.DISCOUNT to discountCode)
- } else {
- mapOf()
- }
-
- return mapOf(
- FirebaseAnalytics.Param.ITEM_ID to sku,
- FirebaseAnalytics.Param.VALUE to value.totalPriceV2.amount,
- FirebaseAnalytics.Param.CURRENCY to value.currencyCode.name,
- ) + discountParams
+ class Builder : AnalyticsHandlerBuilder {
+ override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
+ !data.isDebug -> FirebaseClient()
+ data.isDebug && data.logConfig.firebase -> FirebaseLogClient(data.jsonConverter)
+ else -> null
+ }?.let { FirebaseAnalyticsHandler(it) }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt
index ccacc09f9e..e460f9dbf9 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt
@@ -1,14 +1,17 @@
package com.tangem.tap.common.analytics.handlers.firebase
+import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class FirebaseLogClient(
- private val logger: AnalyticsEventsLogger,
+ jsonConverter: MoshiJsonConverter,
) : FirebaseAnalyticsClient {
+ private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(FirebaseAnalyticsHandler.ID, jsonConverter)
+
override fun logEvent(event: String, params: Map) {
logger.logEvent(event, params)
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt
new file mode 100644
index 0000000000..ffe48cf198
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt
@@ -0,0 +1,21 @@
+package com.tangem.tap.common.analytics.paramsInterceptor
+
+import com.tangem.tap.common.analytics.api.ParamsInterceptor
+import com.tangem.tap.common.analytics.events.AnalyticsEvent
+import com.tangem.tap.common.analytics.events.AnalyticsParam
+
+/**
+[REDACTED_AUTHOR]
+ */
+class BatchIdParamsInterceptor(
+ val batchId: String,
+) : ParamsInterceptor {
+
+ override fun id(): String = this::class.java.simpleName
+
+ override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true
+
+ override fun intercept(params: MutableMap) {
+ params[AnalyticsParam.BatchId] = batchId
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt
index 690f35f388..b20f7470b8 100644
--- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt
+++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt
@@ -1,7 +1,17 @@
package com.tangem.tap.common.compose
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -16,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import com.tangem.core.ui.components.SpacerH8
import com.tangem.tap.common.compose.extensions.stringResourceDefault
import com.tangem.wallet.R
@@ -137,8 +148,7 @@ fun ToggledRippleTheme(
@Preview
@Composable
fun ButtonTest() {
- Scaffold(
- ) {
+ Scaffold {
Column(modifier = Modifier.padding(16.dp)) {
PreviewItem("Button") {
RectangleButton(text = "Some button") {}
diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt
index fc496f1d9b..99042c3391 100644
--- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt
+++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt
@@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
+import com.tangem.core.ui.components.SpacerH16
import com.tangem.domain.DomainDialog
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
@@ -105,7 +106,7 @@ fun SimpleDialog(
modifier = Modifier.padding(22.dp)
) {
DialogTitle(title = title)
- LazyColumn() {
+ LazyColumn {
items(items) { item ->
Row(
modifier = Modifier
@@ -154,4 +155,4 @@ fun ErrorDialog(
}
}
)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt
index 88b088fa6a..2013295f25 100644
--- a/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt
+++ b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt
@@ -9,13 +9,13 @@ import com.tangem.wallet.R
object TangemTypography {
val body1 = TextStyle(
- fontFamily = FontFamily(Font(R.font.roboto)),
+ fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 16.0.sp,
letterSpacing = 0.5.sp,
lineHeight = 24.0.sp,
)
val body2 = TextStyle(
- fontFamily = FontFamily(Font(R.font.roboto)),
+ fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 14.0.sp,
letterSpacing = 0.25.sp,
lineHeight = 20.0.sp,
@@ -27,13 +27,13 @@ object TangemTypography {
lineHeight = 20.0.sp,
)
val caption = TextStyle(
- fontFamily = FontFamily(Font(R.font.roboto)),
+ fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 12.0.sp,
letterSpacing = 0.4000000059604645.sp,
lineHeight = 16.0.sp,
)
val headline1 = TextStyle(
- fontFamily = FontFamily(Font(R.font.roboto)),
+ fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 34.0.sp,
letterSpacing = 0.0.sp,
lineHeight = 44.0.sp,
diff --git a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt
index 07c82d1384..0d19a8e2ea 100644
--- a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt
+++ b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.common.module.ModuleMessage
+import com.tangem.core.ui.components.SpacerH8
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.wallet.R
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
index a9504e668a..711e7ed172 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
@@ -1,10 +1,12 @@
package com.tangem.tap.common.extensions
import androidx.activity.OnBackPressedCallback
+import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
+import com.tangem.feature.referral.ReferralFragment
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
@@ -20,21 +22,22 @@ import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
+import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
+import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
+import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.wallet.R
import timber.log.Timber
-private class Navigation
-
fun FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,
- fgShareTransition: FragmentShareTransition? = null
+ fgShareTransition: FragmentShareTransition? = null,
) {
val transaction = this.supportFragmentManager.beginTransaction()
val fragment = fragmentFactory(screen)
@@ -48,9 +51,18 @@ fun FragmentActivity.openFragment(
}
}
}
- transaction.replace(R.id.fragment_container, fragment, screen.name)
- if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null)
- transaction.commitAllowingStateLoss()
+ if (screen.isDialogFragment) {
+ (fragment as DialogFragment).show(transaction, screen.name)
+ if (addToBackstack) {
+ transaction.addToBackStack(screen.name)
+ }
+ } else {
+ transaction.replace(R.id.fragment_container, fragment, screen.name)
+ if (addToBackstack) {
+ transaction.addToBackStack(screen.name)
+ }
+ transaction.commitAllowingStateLoss()
+ }
}
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
@@ -104,5 +116,9 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()
+ AppScreen.ReferralProgram -> ReferralFragment()
+ AppScreen.Welcome -> WelcomeFragment()
+ AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
+ AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt
index d88560669e..69c5f1c085 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt
@@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
+import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@@ -26,6 +27,11 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
+@Suppress("UnusedReceiverParameter")
+suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
+ store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
+}
+
fun Store<*>.dispatchToastNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowToastNotification(resId))
}
@@ -71,7 +77,6 @@ suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
-
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(url))
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/String.kt b/app/src/main/java/com/tangem/tap/common/extensions/String.kt
index 7d54a7bf92..021cc3a7f2 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/String.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/String.kt
@@ -62,4 +62,9 @@ fun String.toQrCode(): Bitmap {
return bmp
}
-fun String.urlEncode(): String = Uri.encode(this)
\ No newline at end of file
+fun String.urlEncode(): String = Uri.encode(this)
+
+fun String.removePrefixOrNull(prefix: String): String? = when {
+ startsWith(prefix) -> substring(prefix.length)
+ else -> null
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt
index 4be902ebf7..9e58542ecb 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt
@@ -19,6 +19,7 @@ import android.view.inputmethod.InputMethodManager
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
+import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@@ -57,7 +58,7 @@ fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
return context.getString(id, *formatArgs)
}
-fun View.getQuantityString(@StringRes id: Int, quantity: Int): String {
+fun View.getQuantityString(@PluralsRes id: Int, quantity: Int): String {
return context.resources.getQuantityString(id, quantity, quantity)
}
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt
index 15d02907bf..cda489f536 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt
@@ -55,11 +55,10 @@ suspend fun WalletManager.safeUpdate(): Result = try {
}
}
-fun WalletManager?.getToUpUrl(): String? {
+fun WalletManager.getTopUpUrl(): String? {
val globalState = store.state.globalState
- val wallet = this?.wallet ?: return null
-
val defaultAddress = wallet.address
+
return globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,
diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
index 97c8a9e41f..589597238f 100644
--- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
+++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
@@ -8,9 +8,10 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
-import com.tangem.common.card.CardWallet
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.stripZeroPlainString
+import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
class AdditionalFeedbackInfo {
class EmailWalletInfo(
@@ -29,6 +30,7 @@ class AdditionalFeedbackInfo {
var cardFirmwareVersion: String = ""
var cardIssuer: String = ""
var cardBlockchain: String = ""
+ var userWalletId: String = ""
// wallets
internal val walletsInfo = mutableListOf()
@@ -52,6 +54,7 @@ class AdditionalFeedbackInfo {
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = formatSignedHashes(data.card.wallets)
+ userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
}
fun setWalletsInfo(walletManagers: List) {
@@ -89,7 +92,7 @@ class AdditionalFeedbackInfo {
)
}
- private fun formatSignedHashes(wallets: List): String {
+ private fun formatSignedHashes(wallets: List): String {
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
}
diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
index 6578b97356..8a426cccfc 100644
--- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
+++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
@@ -96,8 +96,8 @@ class FeedbackEmail : FeedbackData {
}
class SupportInfo : FeedbackData {
- override val subjectResId: Int = R.string.details_ask_a_question
- override val mainMessageResId: Int = R.string.details_ask_a_question
+ override val subjectResId: Int = R.string.details_chat
+ override val mainMessageResId: Int = R.string.details_chat
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String {
return FeedbackDataBuilder(infoHolder)
diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt
index 46cf302fee..4baf39c3e2 100644
--- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt
+++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt
@@ -22,6 +22,7 @@ class FeedbackDataBuilder(
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
builder.appendKeyValue("", infoHolder.signedHashesCount)
+ builder.appendKeyValue("User Wallet ID", infoHolder.userWalletId)
return this
}
diff --git a/app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt b/app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt
new file mode 100644
index 0000000000..c0ec09ea9c
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt
@@ -0,0 +1,16 @@
+package com.tangem.tap.common.moshi
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.ToJson
+import java.math.BigDecimal
+
+/**
+[REDACTED_AUTHOR]
+ */
+class BigDecimalAdapter {
+ @FromJson
+ fun fromJson(value: String) = BigDecimal(value)
+
+ @ToJson
+ fun toJson(value: BigDecimal) = value.toString()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt
new file mode 100644
index 0000000000..e9aaabfd70
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt
@@ -0,0 +1,27 @@
+package com.tangem.tap.common.redux
+
+import com.tangem.domain.common.ScanResponse
+import com.tangem.tap.common.redux.global.GlobalAction
+import com.tangem.tap.preferencesStorage
+import com.tangem.tap.tangemSdkManager
+import org.rekotlin.Middleware
+
+class AccessCodeRequestPolicyMiddleware {
+ val middleware: Middleware = { _, _ ->
+ { next ->
+ { action ->
+ if (action is GlobalAction.SaveScanResponse) {
+ updateAccessCodeRequestPolicy(action.scanResponse)
+ }
+ next(action)
+ }
+ }
+ }
+
+ private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) {
+ tangemSdkManager.setAccessCodeRequestPolicy(
+ useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
+ scanResponse.card.isAccessCodeSet,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
index cbfa2565fb..f03dfc70ca 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
@@ -10,24 +10,28 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteRedu
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
+import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
+import com.tangem.tap.proxy.AppStateHolder
+import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
+import com.tangem.tap.features.welcome.redux.WelcomeReducer
import org.rekotlin.Action
-fun appReducer(action: Action, state: AppState?): AppState {
+fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState {
requireNotNull(state)
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = NavigationReducer.reduce(action, state),
- globalState = globalReducer(action, state),
+ globalState = globalReducer(action, state, appStateHolder),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
- walletState = WalletReducer.reduce(action, state),
+ walletState = WalletReducer.reduce(action, state, appStateHolder),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
@@ -35,6 +39,9 @@ fun appReducer(action: Action, state: AppState?): AppState {
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
shopState = ShopReducer.reduce(action, state.shopState),
+ welcomeState = WelcomeReducer.reduce(action, state),
+ saveWalletState = SaveWalletReducer.reduce(action, state),
+ walletSelectorState = WalletSelectorReducer.reduce(action, state),
)
}
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
index af79cb81e1..9f7c34b90f 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
@@ -25,6 +25,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
+import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
+import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
@@ -33,6 +35,10 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
+import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
+import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
+import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
+import com.tangem.tap.features.welcome.redux.WelcomeState
import org.rekotlin.Middleware
import org.rekotlin.StateType
@@ -51,6 +57,9 @@ data class AppState(
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(),
+ val welcomeState: WelcomeState = WelcomeState(),
+ val saveWalletState: SaveWalletState = SaveWalletState(),
+ val walletSelectorState: WalletSelectorState = WalletSelectorState(),
) : StateType {
val domainState: DomainState
@@ -62,7 +71,9 @@ data class AppState(
companion object {
fun getMiddleware(): List> {
return listOf(
- logMiddleware, navigationMiddleware, notificationsMiddleware,
+ logMiddleware,
+ navigationMiddleware,
+ notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
@@ -78,8 +89,12 @@ data class AppState(
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
+ WelcomeMiddleware().middleware,
+ SaveWalletMiddleware().middleware,
+ WalletSelectorMiddleware().middleware,
+ LockUserWalletsTimerMiddleware().middleware,
+ AccessCodeRequestPolicyMiddleware().middleware,
)
}
}
-}
-
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/redux/LockUserWalletsTimerMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LockUserWalletsTimerMiddleware.kt
new file mode 100644
index 0000000000..67bb53a5b5
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/redux/LockUserWalletsTimerMiddleware.kt
@@ -0,0 +1,15 @@
+package com.tangem.tap.common.redux
+
+import com.tangem.tap.lockUserWalletsTimer
+import org.rekotlin.Middleware
+
+class LockUserWalletsTimerMiddleware {
+ val middleware: Middleware = { _, _ ->
+ { nextDispatch ->
+ { action ->
+ lockUserWalletsTimer?.restart()
+ nextDispatch(action)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt
index b226be3401..1f51374cae 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt
@@ -5,7 +5,6 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
-import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
@@ -30,9 +29,6 @@ sealed class GlobalAction : Action {
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
- // android resources
- data class SetResources(val resources: AndroidResources) : GlobalAction()
-
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
@@ -68,7 +64,7 @@ sealed class GlobalAction : Action {
object Increment : GlobalAction()
}
- data class SaveScanNoteResponse(val scanResponse: ScanResponse) : GlobalAction()
+ data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
data class SetIfCardVerifiedOnline(val verified: Boolean) : GlobalAction()
@@ -89,7 +85,6 @@ sealed class GlobalAction : Action {
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
- data class SetGlobalAnalyticsHandler(val analyticsHandler: GlobalAnalyticsEventHandler) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction()
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt
index 6f5af6bb92..dfcdf942bf 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt
@@ -167,10 +167,9 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
scope.launch {
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
- store.state.globalState.analyticsHandler,
- userTokensRepository,
- action.additionalBlockchainsToDerive,
- action.messageResId,
+ userTokensRepository = userTokensRepository,
+ additionalBlockchainsToDerive = action.additionalBlockchainsToDerive,
+ messageRes = action.messageResId,
)
withMainContext {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt
index b0cefc6d9e..543e734f8f 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt
@@ -2,21 +2,20 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
+import com.tangem.tap.common.extensions.replaceBy
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
+import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.Action
-fun globalReducer(action: Action, state: AppState): GlobalState {
+fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolder): GlobalState {
if (action !is GlobalAction) return state.globalState
val globalState = state.globalState
return when (action) {
- is GlobalAction.SetResources -> {
- globalState.copy(resources = action.resources)
- }
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
@@ -34,7 +33,8 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.ScanFailsCounter.Reset -> {
globalState.copy(scanCardFailsCounter = 0)
}
- is GlobalAction.SaveScanNoteResponse ->{
+ is GlobalAction.SaveScanResponse -> {
+ appStateHolder.scanResponse = action.scanResponse
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
globalState.copy(scanResponse = action.scanResponse)
}
@@ -48,15 +48,22 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(configManager = action.configManager)
}
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
- is GlobalAction.SetGlobalAnalyticsHandler -> globalState.copy(analyticsHandler = action.analyticsHandler)
is GlobalAction.UpdateWalletSignedHashes -> {
val card = globalState.scanResponse?.card ?: return globalState
- val wallet = card.wallet(action.walletPublicKey) ?: return globalState
+ val wallet = card.wallets
+ .firstOrNull { it.publicKey.contentEquals(action.walletPublicKey) }
+ ?: return globalState
- val newCardInstance = card.updateWallet(wallet.copy(
- totalSignedHashes = action.walletSignedHashes,
- remainingSignatures = action.remainingSignatures
- ))
+ val newCardInstance = card.copy(
+ wallets = card.wallets.toMutableList().also { walletsMutable ->
+ walletsMutable.replaceBy(
+ item = wallet.copy(
+ totalSignedHashes = action.walletSignedHashes,
+ remainingSignatures = action.remainingSignatures,
+ ),
+ ) { it.index == wallet.index }
+ },
+ )
globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance))
}
is GlobalAction.SetFeedbackManager -> {
@@ -74,7 +81,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
is GlobalAction.FetchUserCountry.Success -> globalState.copy(
- userCountryCode = action.countryCode
+ userCountryCode = action.countryCode,
)
else -> globalState
}
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt
index a02172db23..b897f1b5dc 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt
@@ -1,7 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.common.ScanResponse
-import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
@@ -26,20 +25,9 @@ data class GlobalState(
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
- val resources: AndroidResources = AndroidResources(),
- val analyticsHandler: GlobalAnalyticsEventHandler = GlobalAnalyticsEventHandler.stub(),
val userCountryCode: String? = null,
) : StateType
-
-data class AndroidResources(
- val strings: RString = RString(),
-) {
- data class RString(
- val addressWasCopied: Int = -1,
- val walletIsNotEmpty: Int = -1,
- )
-}
typealias CryptoCurrencyName = String
data class OnboardingState(
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt
index e397d78d8a..3e4f3a9702 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt
@@ -12,12 +12,17 @@ sealed class NavigationAction : Action {
val addToBackstack: Boolean = true,
) : NavigationAction()
- data class PopBackTo(val screen: AppScreen? = null) : NavigationAction()
+ data class PopBackTo(
+ val screen: AppScreen? = null,
+ val inclusive: Boolean = false,
+ ) : NavigationAction()
data class OpenUrl(val url: String) : NavigationAction()
data class OpenDocument(val url: Uri) : NavigationAction()
+ object OpenBiometricsSettings : NavigationAction()
+
data class Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference) : NavigationAction()
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt
index 8a2f5c5ac1..28357c1efd 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt
@@ -1,14 +1,20 @@
package com.tangem.tap.common.redux.navigation
import android.content.Intent
+import android.hardware.biometrics.BiometricManager
+import android.os.Build
+import android.provider.Settings
+import com.tangem.tap.activityResultCaller
import com.tangem.tap.common.CustomTabsManager
+import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.redux.AppState
+import com.tangem.tap.store
import org.rekotlin.Middleware
-val navigationMiddleware: Middleware = { dispatch, state ->
+val navigationMiddleware: Middleware = { _, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
@@ -16,16 +22,24 @@ val navigationMiddleware: Middleware = { dispatch, state ->
when (action) {
is NavigationAction.NavigateTo -> {
navState?.activity?.get()?.openFragment(
- action.screen,
- action.addToBackstack,
- action.fragmentShareTransition
+ screen = action.screen,
+ addToBackstack = action.addToBackstack,
+ fgShareTransition = action.fragmentShareTransition,
)
}
is NavigationAction.PopBackTo -> {
- if (action.screen == AppScreen.Home) {
- navState?.activity?.get()?.popBackTo(null, true)
- } else {
- navState?.activity?.get()?.popBackTo(action.screen)
+ if (navState?.backStack?.lastOrNull() != action.screen) {
+ when (val screen = action.screen) {
+ AppScreen.Home,
+ AppScreen.Welcome,
+ -> {
+ navState?.activity?.get()?.popBackTo(screen, inclusive = true)
+ store.dispatchOnMain(NavigationAction.NavigateTo(screen))
+ }
+ else -> {
+ navState?.activity?.get()?.popBackTo(screen, action.inclusive)
+ }
+ }
}
}
is NavigationAction.OpenUrl -> {
@@ -38,11 +52,31 @@ val navigationMiddleware: Middleware = { dispatch, state ->
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
- is NavigationAction.Share -> {
- navState?.activity?.get()?.let {
- it.shareText(action.data)
+ is NavigationAction.OpenBiometricsSettings -> {
+ val settingsAction = when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
+ Settings.ACTION_BIOMETRIC_ENROLL
+ }
+ else -> {
+ Settings.ACTION_SECURITY_SETTINGS
+ }
}
+ val intent = Intent(settingsAction).apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ putExtra(
+ Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
+ BiometricManager.Authenticators.BIOMETRIC_STRONG,
+ )
+ }
+ }
+ activityResultCaller.activityResultLauncher?.launch(intent)
}
+ is NavigationAction.Share -> {
+ navState?.activity?.get()?.shareText(action.data)
+ }
+ is NavigationAction.ActivityCreated,
+ is NavigationAction.ActivityDestroyed,
+ -> Unit
}
}
next(action)
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt
index 7d63cb95ee..b6f7522586 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt
@@ -20,6 +20,8 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
navState.copy(backStack = navState.backStack + navigationAction.screen)
}
is NavigationAction.PopBackTo -> {
+ if (navState.backStack.lastOrNull() == navigationAction.screen) return navState
+
val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val index = navState.backStack.lastIndexOf(screen) + 1
state.navigationState.copy(backStack = navState.backStack.subList(0, index))
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
index 224e12755b..cf60dc79f6 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
@@ -5,11 +5,13 @@ import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
- val backStack: List = listOf(AppScreen.Home),
+ val backStack: List = emptyList(),
val activity: WeakReference? = null,
) : StateType
-enum class AppScreen {
+enum class AppScreen(
+ val isDialogFragment: Boolean = false,
+) {
Home,
Shop,
Disclaimer,
@@ -19,5 +21,9 @@ enum class AppScreen {
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory,
AddTokens, AddCustomToken,
WalletConnectSessions,
- QrScan
+ QrScan,
+ ReferralProgram,
+ Welcome,
+ SaveWallet(isDialogFragment = true),
+ WalletSelector(isDialogFragment = true),
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt
index 94c37e96d9..808cb5bbbc 100644
--- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt
+++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt
@@ -4,8 +4,8 @@ import android.app.Application
import android.content.Intent
import com.google.android.gms.wallet.PaymentData
import com.shopify.buy3.Storefront
-import com.tangem.common.extensions.ifNotNull
-import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
+import com.tangem.tap.common.analytics.Analytics
+import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
@@ -23,7 +23,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private val shopifyService = ShopifyService(application, shopifyShop)
-
private val checkouts = mutableMapOf()
private val variants = mutableMapOf()
@@ -41,24 +40,27 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
variants.putAll(availableVariants)
if (variants.size < SKUS_TO_DISPLAY.size) {
- return Result.failure(Exception(
- "Shopify: products are missing, " +
- "\nproducts available: ${variants.keys.map { it.sku }}"))
+ return Result.failure(
+ Exception(
+ "Shopify: products are missing, " +
+ "\nproducts available: ${variants.keys.map { it.sku }}",
+ ),
+ )
}
val twoCardsProduct = TangemProduct(
type = ProductType.WALLET_2_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(),
- beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format()
- )
+ beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format(),
+ ),
)
val threeCardsProduct = TangemProduct(
type = ProductType.WALLET_3_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(),
- beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format()
- )
+ beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format(),
+ ),
)
createCheckouts()
return Result.success(listOf(twoCardsProduct, threeCardsProduct))
@@ -86,7 +88,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice, currencyCode = checkouts[productType]!!.currencyCode.name,
- merchantID = shopifyService.shop.merchantID
+ merchantID = shopifyService.shop.merchantID,
)
}
@@ -105,7 +107,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
suspend fun handleGooglePayResult(
resultCode: Int,
data: Intent?,
- productType: ProductType
+ productType: ProductType,
): Result {
val result = googlePayService.handleResponseFromGooglePay(resultCode, data)
result.onSuccess {
@@ -120,7 +122,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private suspend fun completeTokenizedPayment(
paymentData: PaymentData,
- productType: ProductType
+ productType: ProductType,
): Result {
val checkout = checkouts[productType]!!
val googlePayResponse =
@@ -141,19 +143,18 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
phone = addressGPay.phoneNumber
}
-
val payment = Storefront.TokenizedPaymentInputV3(
amount,
idempotencyKey,
address,
paymentData.toJson(),
- Storefront.PaymentTokenType.GOOGLE_PAY
+ Storefront.PaymentTokenType.GOOGLE_PAY,
)
.setTest(true)
return shopifyService.completeWithTokenizedPayment(
payment = payment,
- checkoutID = checkout.id
+ checkoutID = checkout.id,
)
}
@@ -182,12 +183,12 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
productType,
TotalSum(
finalValue = it.totalPriceV2.format(),
- beforeDiscount = variants[productType]!!.compareAtPriceV2.format()
+ beforeDiscount = variants[productType]!!.compareAtPriceV2.format(),
),
- appliedDiscount = it.getAppliedDiscount()
- )
+ appliedDiscount = it.getAppliedDiscount(),
+ ),
- )
+ )
}
return Result.failure(result.exceptionOrNull()!!)
}
@@ -196,10 +197,13 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
return checkouts[productType]!!.webUrl
}
- suspend fun waitForCheckout(productType: ProductType, analyticsHandler: GlobalAnalyticsEventHandler?) {
+ suspend fun waitForCheckout(productType: ProductType) {
val result = shopifyService.checkout(true, checkouts[productType]!!.id)
- result.onSuccess {
- ifNotNull(analyticsHandler, it.order) { handler, order -> handler.handleShopifyOrderEvent(order) }
+ result.onSuccess { checkout ->
+ checkout.order?.let {
+ val event = ShopOrderToEventConverter().convert(it to productType)
+ Analytics.send(event)
+ }
}
}
diff --git a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt b/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt
index 8b4136e90f..7cee3bcfd9 100644
--- a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt
@@ -2,7 +2,7 @@ package com.tangem.tap.domain
import com.tangem.common.services.Result
import com.tangem.domain.common.ThrottlerWithValues
-import com.tangem.network.api.tangemTech.TangemTechService
+import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import java.math.BigDecimal
diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
index 70576a26b9..be8e4f65b2 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
@@ -1,7 +1,5 @@
package com.tangem.tap.domain
-import CreateProductWalletTask
-import CreateProductWalletTaskResponse
import android.content.Context
import androidx.annotation.StringRes
import com.tangem.Message
@@ -10,14 +8,20 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.SuccessResponse
-import com.tangem.common.card.Card
+import com.tangem.common.UserCode
+import com.tangem.common.UserCodeType
+import com.tangem.common.biometric.BiometricManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.Config
import com.tangem.common.core.TangemSdkError
+import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.common.map
+import com.tangem.common.usersCode.UserCodeRepository
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.operations.CommandResponse
import com.tangem.operations.ScanTask
@@ -26,15 +30,14 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.CheckUserCodesCommand
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.operations.pins.SetUserCodeCommand
-import com.tangem.tap.common.analytics.AnalyticsAnOld
-import com.tangem.tap.common.analytics.AnalyticsEventAnOld
-import com.tangem.tap.common.analytics.AnalyticsParamAnOld
-import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
+import com.tangem.tap.common.analytics.Analytics
+import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
+import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
+import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tokens.UserTokensRepository
-import com.tangem.tap.features.demo.DemoHelper
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
@@ -44,19 +47,38 @@ import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
+ private val userCodeRepository by lazy {
+ UserCodeRepository(
+ biometricManager = tangemSdk.biometricManager,
+ secureStorage = tangemSdk.secureStorage,
+ )
+ }
+
+ val canUseBiometry: Boolean
+ get() = tangemSdk.biometricManager.canAuthenticate || needEnrollBiometrics
+
+ val needEnrollBiometrics: Boolean
+ get() = tangemSdk.biometricManager.canEnrollBiometrics
+
+ val biometricManager: BiometricManager
+ get() = tangemSdk.biometricManager
+
suspend fun scanProduct(
- analyticsHandler: GlobalAnalyticsEventHandler?,
userTokensRepository: UserTokensRepository,
+ cardId: String? = null,
additionalBlockchainsToDerive: Collection? = null,
messageRes: Int? = null,
): CompletionResult {
- analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.READY_TO_SCAN, card = null)
-
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
- runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
- cardId = null, initialMessage = message,
- ).also { sendScanResultsToAnalytics(analyticsHandler, it) }
+ runnable = ScanProductTask(
+ card = null,
+ userTokensRepository = userTokensRepository,
+ additionalBlockchainsToDerive = additionalBlockchainsToDerive,
+ ),
+ cardId = cardId,
+ initialMessage = message,
+ ).also { sendScanResultsToAnalytics(it) }
}
suspend fun createProductWallet(
@@ -70,41 +92,22 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
private fun sendScanResultsToAnalytics(
- analyticsHandler: GlobalAnalyticsEventHandler?,
result: CompletionResult,
) {
- when (result) {
- is CompletionResult.Success -> {
- analyticsHandler?.handleAnalyticsEvent(
- event = AnalyticsEventAnOld.CARD_IS_SCANNED,
- card = result.data.card,
- blockchain = result.data.walletData?.blockchain,
- )
- if (DemoHelper.isDemoCard(result.data)) {
- analyticsHandler?.handleAnalyticsEvent(
- event = AnalyticsEventAnOld.DEMO_MODE_ACTIVATED,
- params = mapOf(AnalyticsParamAnOld.CARD_ID.param to result.data.card.cardId),
- card = result.data.card,
- blockchain = result.data.walletData?.blockchain,
- )
- }
+ if (result is CompletionResult.Failure) {
+ (result.error as? TangemSdkError)?.let { error ->
+ Analytics.send(Basic.ScanError(error))
}
- is CompletionResult.Failure ->
- (result.error as? TangemSdkError)?.let { error ->
- analyticsHandler?.handleCardSdkErrorEvent(
- error = error,
- action = AnalyticsAnOld.ActionToLog.Scan,
- )
- }
}
}
- suspend fun createWallet(cardId: String?): CompletionResult {
+ suspend fun createWallet(cardId: String?): CompletionResult {
return runTaskAsyncReturnOnMain(
CreateWalletAndRescanTask(),
cardId,
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)),
)
+ .map { CardDTO(it) }
}
suspend fun derivePublicKeys(
@@ -114,12 +117,31 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
- suspend fun resetToFactorySettings(card: Card): CompletionResult {
+ suspend fun resetToFactorySettings(cardId: String): CompletionResult {
return runTaskAsyncReturnOnMain(
- ResetToFactorySettingsTask(),
- card.cardId,
- initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings)),
+ runnable = ResetToFactorySettingsTask(),
+ cardId = cardId,
+ initialMessage = Message(context.getString(R.string.card_settings_reset_card_to_factory)),
)
+ .map { CardDTO(it) }
+ }
+
+ suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult {
+ return userCodeRepository.save(
+ cardsIds = cardsIds,
+ userCode = UserCode(
+ type = UserCodeType.AccessCode,
+ stringValue = accessCode,
+ ),
+ )
+ }
+
+ suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult {
+ return userCodeRepository.delete(cardsIds.toSet())
+ }
+
+ suspend fun clearSavedUserCodes(): CompletionResult {
+ return userCodeRepository.clear()
}
suspend fun setPasscode(cardId: String?): CompletionResult {
@@ -154,11 +176,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
)
}
- suspend fun scanCard(): CompletionResult {
+ suspend fun scanCard(
+ cardId: String? = null,
+ allowRequestAccessCodeFromRepository: Boolean = false,
+ ): CompletionResult {
return runTaskAsyncReturnOnMain(
- ScanTask(),
+ runnable = ScanTask(allowRequestAccessCodeFromRepository),
+ cardId = cardId,
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
)
+ .map { CardDTO(it) }
}
suspend fun runTaskAsync(
@@ -176,7 +203,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
private suspend fun runTaskAsyncReturnOnMain(
- runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null,
+ runnable: CardSessionRunnable,
+ cardId: String? = null,
+ initialMessage: Message? = null,
): CompletionResult {
val result = runTaskAsync(runnable, cardId, initialMessage)
return withContext(Dispatchers.Main) { result }
@@ -192,7 +221,22 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
- return context.getString(stringResId, formatArgs)
+ return context.getString(stringResId, *formatArgs)
+ }
+
+ fun setAccessCodeRequestPolicy(
+ useBiometricsForAccessCode: Boolean,
+ ) {
+ tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) {
+ UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode)
+ } else {
+ UserCodeRequestPolicy.Default
+ }
+ }
+
+ fun useBiometricsForAccessCode(): Boolean {
+ val policy = tangemSdk.config.userCodeRequestPolicy
+ return policy is UserCodeRequestPolicy.AlwaysWithBiometrics && policy.codeType == UserCodeType.AccessCode
}
companion object {
diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
index af5cdb6a5c..1979b1df8a 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
@@ -5,13 +5,13 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
-import com.tangem.common.card.Card
+import com.tangem.domain.common.CardDTO
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
- private val card: Card,
+ private val card: CardDTO,
private val tangemSdk: TangemSdk,
private val initialMessage: Message,
private val accessCode: String? = null,
diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt
index 5a0cf85668..aae3cf8e9a 100644
--- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt
@@ -2,7 +2,7 @@ package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.common.core.TangemError
-import com.tangem.network.api.tangemTech.TangemTechError
+import com.tangem.datasource.api.tangemTech.TangemTechError
import com.tangem.wallet.R
interface TapErrors
@@ -64,8 +64,7 @@ sealed class TapError(
) : TapError(-1), MultiMessageError
}
-sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError {
- final override val code: Int = 50100
+sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) {
override var customMessage: String = code.toString()
object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card)
@@ -88,4 +87,4 @@ fun TangemTechError.toTapError(): TapError {
}
}
-class NoDataError(message: String) : TapError.CustomError(customMessage = message)
+class NoDataError(message: String) : TapError.CustomError(customMessage = message)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
index b15f2a7431..ea26d15848 100644
--- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
@@ -6,13 +6,16 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
-import com.tangem.common.card.Card
+import com.tangem.common.doOnFailure
+import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
@@ -20,16 +23,22 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
+import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
+import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
+import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
+import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
+import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import timber.log.Timber
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
@@ -81,22 +90,75 @@ class TapWalletManager {
}
}
+ suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) {
+ val scanResponse = userWallet.scanResponse
+ val card = scanResponse.card
+ val attestationFailed = card.attestation.status == Attestation.Status.Failed
+
+ tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
+ store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
+ updateConfigManager(scanResponse)
+
+ withMainContext {
+ store.dispatch(WalletAction.UserWalletChanged(userWallet))
+ store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
+ store.dispatch(WalletConnectAction.ResetState)
+ store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
+ store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
+ store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
+ store.dispatch(WalletAction.Warnings.CheckIfNeeded)
+ }
+
+ loadData(userWallet, refresh)
+ }
+
+ suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
+ walletStoresManager.fetch(userWallet, refresh)
+ .doOnSuccess {
+ Timber.d("Wallet stores fetched for ${userWallet.walletId}")
+ store.dispatchOnMain(WalletAction.LoadData.Success)
+ }
+ .doOnFailure { error ->
+ val errorAction = when (error) {
+ is WalletStoresError -> when (error) {
+ is WalletStoresError.FetchFiatRatesError,
+ is WalletStoresError.UpdateWalletManagerError,
+ -> WalletAction.LoadData.Failure(error = null)
+ is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
+ error = TapError.WalletManager.CreationError,
+ )
+ is WalletStoresError.UnknownBlockchain -> WalletAction.LoadData.Failure(
+ error = TapError.UnknownBlockchain,
+ )
+ is WalletStoresError.NoInternetConnection -> WalletAction.LoadData.Failure(
+ error = TapError.NoInternetConnection,
+ )
+ }
+ else -> WalletAction.LoadData.Failure(error = null)
+ }
+
+ Timber.e(error, "Wallet stores fetching failed for ${userWallet.walletId}")
+
+ store.dispatchOnMain(errorAction)
+ }
+ }
+
suspend fun onCardScanned(data: ScanResponse) {
walletManagersThrottler.clear()
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
updateConfigManager(data)
withMainContext {
- store.dispatch(WalletAction.ResetState(data.card.cardId))
+ store.dispatch(WalletAction.ResetState(data.card))
store.dispatch(WalletConnectAction.ResetState)
- store.dispatch(GlobalAction.SaveScanNoteResponse(data))
+ store.dispatch(GlobalAction.SaveScanResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
store.dispatch(WalletConnectAction.RestoreSessions(data))
store.dispatch(
WalletAction.MultiWallet.ShowWalletBackupWarning(
show = data.card.settings.isBackupAllowed
- && data.card.backupStatus == Card.BackupStatus.NoBackup,
+ && data.card.backupStatus == CardDTO.BackupStatus.NoBackup,
),
)
loadData(data)
@@ -143,7 +205,7 @@ class TapWalletManager {
private fun checkIfDerivationsAreMissing(blockchainNetworks: List, scanResponse: ScanResponse) {
blockchainNetworks.map {
if (it.tokens.isNotEmpty()) {
- WalletAction.MultiWallet.AddTokens(it.tokens, it, false)
+ WalletAction.MultiWallet.AddTokens(it.tokens, it)
}
}
val missingDerivations = blockchainNetworks
@@ -171,7 +233,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
walletManagers = listOf(primaryWalletManager),
- save = false,
),
WalletAction.LoadFiatRate(),
)
@@ -187,7 +248,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(
blockchains = blockchainNetworks,
walletManagers = walletManagers,
- save = false,
),
)
@@ -197,7 +257,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddTokens(
tokens = it.tokens,
blockchain = it,
- save = false,
),
)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt
index 683302ba73..f75a4922ce 100644
--- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt
@@ -77,7 +77,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
- R.string.alert_title,
+ R.string.common_warning,
R.string.alert_developer_card,
WarningMessage.Origin.Local,
)
@@ -89,7 +89,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
- R.string.alert_title,
+ R.string.common_warning,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local,
)
@@ -142,7 +142,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
blockchains = null,
- titleResId = R.string.alert_title,
+ titleResId = R.string.common_warning,
messageResId = R.string.warning_low_signatures_format,
origin = WarningMessage.Origin.Local,
messageFormatArg = remainingSignatures.toString(),
@@ -155,7 +155,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
null,
- R.string.alert_title,
+ R.string.common_warning,
R.string.warning_testnet_card_message,
WarningMessage.Origin.Local,
)
@@ -167,7 +167,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
- R.string.alert_title,
+ R.string.common_warning,
R.string.alert_demo_message,
WarningMessage.Origin.Local,
)
diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
index ab2ba5610b..fd46ad2fde 100644
--- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
+++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
@@ -1,58 +1,45 @@
package com.tangem.tap.domain.extensions
-import com.tangem.common.card.Card
-import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
-import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.TwinCardNumber
-import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.isTangemTwin
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.features.wallet.redux.Artwork
-val Card.remainingSignatures: Int?
- get() = this.getSingleWallet()?.remainingSignatures
+val CardDTO.remainingSignatures: Int?
+ get() = this.wallets.firstOrNull()?.remainingSignatures
-val Card.isWalletDataSupported: Boolean
+val CardDTO.isWalletDataSupported: Boolean
get() = this.firmwareVersion.major >= 4
-val Card.isMultiwalletAllowed: Boolean
+val CardDTO.isMultiwalletAllowed: Boolean
get() {
- return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay
- && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
- getSingleWallet()?.curve == EllipticCurve.Secp256k1)
+ return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay &&
+ (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
+ wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1)
}
-val Card.isHdWalletAllowedByApp: Boolean
+val CardDTO.isHdWalletAllowedByApp: Boolean
get() = settings.isHDWalletAllowed && !isSaltPay
-fun Card.getSingleWallet(): CardWallet? {
- return wallets.firstOrNull()
+fun CardDTO.hasSignedHashes(): Boolean {
+ return wallets.any { (it.totalSignedHashes ?: 0) > 0 }
}
-fun Card.hasWallets(): Boolean = wallets.isNotEmpty()
-
-fun Card.hasNoWallets(): Boolean = wallets.isEmpty()
-
-fun Card.hasSingleWallet(): Boolean = wallets.size == 1
-
-fun Card.hasSignedHashes(): Boolean {
- return wallets.any { it.totalSignedHashes ?: 0 > 0 }
+fun CardDTO.signedHashesCount(): Int {
+ return wallets.sumOf { it.totalSignedHashes ?: 0 }
}
-fun Card.signedHashesCount(): Int {
- return wallets.map { it.totalSignedHashes ?: 0 }.sum()
-}
-
-suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String {
+suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String {
fun ifAnyError(): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
@@ -67,42 +54,28 @@ suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result {
val artworkId = cardInfoResult.data.artwork?.id
- if (artworkId == null || artworkId.isEmpty()) {
+ if (artworkId.isNullOrEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
+
is Result.Failure -> ifAnyError()
}
}
-fun Card.getArtworkUrl(artworkId: String?): String? {
+fun CardDTO.getArtworkUrl(artworkId: String?): String? {
return when {
artworkId != null -> {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
+
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> null
}
-}
-
-fun Card.getUserWalletId(): String {
- val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return ""
- return UserWalletId(walletPublicKey).stringValue
-}
-
-class UserWalletId(val walletPublicKey: ByteArray) {
- val stringValue: String = calculateUserId(walletPublicKey)
-
- private fun calculateUserId(walletPublicKey: ByteArray): String {
- val message = "UserWalletID".toByteArray()
- val keyHash = walletPublicKey.calculateSha256()
- return message.calculateHmacSha256(keyHash).toHexString()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
index ec95e13dc6..874e562a2a 100644
--- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
+++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
@@ -7,13 +7,12 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
-import com.tangem.common.card.Card
-import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
@@ -82,7 +81,7 @@ fun WalletManagerFactory.makeWalletManagerForApp(
)
}
-private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? {
+private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? {
return derivationPath?.let {
DerivationParams.Custom(
DerivationPath(it),
@@ -132,7 +131,7 @@ fun WalletManagerFactory.makePrimaryWalletManager(
)
}
-private fun selectWallet(wallets: List): CardWallet? {
+private fun selectWallet(wallets: List): CardDTO.Wallet? {
return when (wallets.size) {
0 -> null
1 -> wallets[0]
diff --git a/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt
new file mode 100644
index 0000000000..dd9a0b27a1
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt
@@ -0,0 +1,21 @@
+package com.tangem.tap.domain.model
+
+import java.math.BigDecimal
+
+/**
+ * Represents fiat balance of [WalletStoreModel] list
+ * @property amount Amount of the total balance
+ * */
+sealed class TotalFiatBalance {
+ open val amount: BigDecimal = BigDecimal.ZERO
+
+ object Loading : TotalFiatBalance()
+
+ data class Error(
+ override val amount: BigDecimal,
+ ) : TotalFiatBalance()
+
+ data class Loaded(
+ override val amount: BigDecimal,
+ ) : TotalFiatBalance()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
new file mode 100644
index 0000000000..a76fbb9d1a
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
@@ -0,0 +1,35 @@
+package com.tangem.tap.domain.model
+
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.UserWalletId
+
+/**
+ * Represents user's wallet which stored in app persistence
+ * @param name User wallet name
+ * @param walletId User wallet [UserWalletId]
+ * @param artworkUrl User wallet card artwork URL
+ * @param cardsInWallet List of cards IDs assigned with this user's wallet
+ * @param isMultiCurrency Indicates whether this user wallet can work with more than one currency
+ * @param scanResponse [ScanResponse] of primary user's wallet card.
+ * TODO: Replace with [com.tangem.domain.common.CardDTO]
+ * @property cardId ID of user's wallet primary card
+ * @property hasAccessCode Indicates if the user's wallet primary card has access code
+ * @property isLocked Indicates if this primary card has no currency wallets
+ * */
+data class UserWallet(
+ val name: String,
+ val walletId: UserWalletId,
+ val artworkUrl: String,
+ val cardsInWallet: Set,
+ val isMultiCurrency: Boolean,
+ val scanResponse: ScanResponse,
+) {
+ val cardId: String
+ get() = scanResponse.card.cardId
+
+ val hasAccessCode: Boolean
+ get() = scanResponse.card.isAccessCodeSet
+
+ val isLocked: Boolean
+ get() = scanResponse.card.wallets.isEmpty()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt
new file mode 100644
index 0000000000..4462f4d8ec
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt
@@ -0,0 +1,71 @@
+package com.tangem.tap.domain.model
+
+import com.tangem.tap.domain.model.WalletDataModel.Status
+import com.tangem.tap.features.wallet.models.Currency
+import com.tangem.tap.features.wallet.models.PendingTransaction
+import com.tangem.tap.features.wallet.redux.AddressData
+import java.math.BigDecimal
+
+/**
+ * Contains info about wallet store currency
+ * @param currency Wallet's [Currency]
+ * @param status Wallet's [Status], represents current status of that currency
+ * @param walletAddresses List of wallet data [AddressData]
+ * @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all
+ * founds will be destroyed. Null if currency don't have existential deposit
+ * @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided
+ * */
+data class WalletDataModel(
+ val currency: Currency,
+ val status: Status,
+ val walletAddresses: List,
+ val existentialDeposit: BigDecimal?,
+ val fiatRate: BigDecimal?,
+) {
+
+ /**
+ * Represent current status of currency
+ * @property amount Currency amount
+ * @property pendingTransactions List of currency [PendingTransaction] sent in currency's blockchain
+ * @property errorMessage Status error message, null if not provided
+ * @property isErrorStatus true if current status is error status, false otherwise
+ * */
+ sealed class Status {
+ open val amount: BigDecimal = BigDecimal.ZERO
+ open val pendingTransactions: List = emptyList()
+ open val errorMessage: String? = null
+ open val isErrorStatus: Boolean = false
+ }
+
+ object Loading : Status()
+
+ data class VerifiedOnline(
+ override val amount: BigDecimal,
+ ) : Status()
+
+ data class TransactionInProgress(
+ override val amount: BigDecimal,
+ override val pendingTransactions: List,
+ ) : Status()
+
+ data class SameCurrencyTransactionInProgress(
+ override val amount: BigDecimal,
+ override val pendingTransactions: List,
+ ) : Status()
+
+ data class NoAccount(
+ val amountToCreateAccount: BigDecimal?,
+ ) : Status() {
+ override val isErrorStatus: Boolean = true
+ }
+
+ data class Unreachable(
+ override val errorMessage: String?,
+ ) : Status() {
+ override val isErrorStatus: Boolean = true
+ }
+
+ object MissedDerivation : Status() {
+ override val isErrorStatus: Boolean = true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt
new file mode 100644
index 0000000000..a0511784e2
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt
@@ -0,0 +1,77 @@
+package com.tangem.tap.domain.model
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.WalletManager
+import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.WalletStoreModel.WalletRent
+import com.tangem.tap.domain.tokens.models.BlockchainNetwork
+import java.math.BigDecimal
+
+/**
+ * Contains info about the blockchain and its currencies
+ * @param userWalletId ID of the associated [UserWallet]
+ * @param blockchain [Blockchain] of this WalletStore
+ * @param derivationPath [DerivationPath] of this store, null if the card does not support the
+ * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/)
+ * @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies
+ * @param walletRent [WalletRent], null if store has no rent or currency balance is greater then
+ * [WalletRent.exemptionAmount]
+ * @param blockchainNetwork [BlockchainNetwork].
+ * TODO: Remove after WalletMiddleware refactoring
+ * @param walletManager [WalletManager], may be null if it fails to create this manager.
+ * TODO: Remove after WalletMiddleware refactoring
+ * */
+data class WalletStoreModel(
+ val userWalletId: UserWalletId,
+ val blockchain: Blockchain,
+ val derivationPath: DerivationPath?,
+ val walletsData: List,
+ val walletRent: WalletRent?,
+ @Deprecated("Don't use it, will be removed")
+ val blockchainNetwork: BlockchainNetwork,
+ @Deprecated("Don't use it, will be removed")
+ val walletManager: WalletManager?,
+) {
+
+ /**
+ * Represents wallet blockchain rent
+ * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than
+ * the [WalletRent.exemptionAmount]
+ * @param exemptionAmount Amount that should be on the blockchain balance not to pay rent
+ * */
+ data class WalletRent(
+ val rent: BigDecimal,
+ val exemptionAmount: BigDecimal,
+ )
+
+ // TODO: Remove the generated methods after blockchainNetwork and walletManager are removed from the model
+ // region Generated
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is WalletStoreModel) return false
+
+ if (userWalletId != other.userWalletId) return false
+ if (blockchain != other.blockchain) return false
+ if (derivationPath != other.derivationPath) return false
+ if (walletsData != other.walletsData) return false
+ if (walletRent != other.walletRent) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = userWalletId.hashCode()
+ result = 31 * result + blockchain.hashCode()
+ result = 31 * result + (derivationPath?.hashCode() ?: 0)
+ result = 31 * result + walletsData.hashCode()
+ result = 31 * result + (walletRent?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString(): String {
+ return "WalletStoreModel(userWalletId=$userWalletId, blockchain=$blockchain, derivationPath=$derivationPath, " +
+ "walletsData=$walletsData, walletRent=$walletRent)"
+ }
+ // endregion Generated
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt
new file mode 100644
index 0000000000..9148486bc6
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt
@@ -0,0 +1,110 @@
+package com.tangem.tap.domain.model.builders
+
+import com.tangem.common.card.EllipticCurve
+import com.tangem.common.card.FirmwareVersion
+import com.tangem.common.extensions.toHexString
+import com.tangem.common.services.Result
+import com.tangem.domain.common.CardDTO
+import com.tangem.domain.common.ProductType
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.TapWorkarounds.isStart2Coin
+import com.tangem.domain.common.TwinCardNumber
+import com.tangem.domain.common.TwinsHelper
+import com.tangem.operations.attestation.OnlineCardVerifier
+import com.tangem.operations.attestation.TangemApi
+import com.tangem.tap.domain.model.UserWallet
+import com.tangem.tap.features.wallet.redux.Artwork
+
+class UserWalletBuilder(
+ private val scanResponse: ScanResponse,
+ private val onlineCardVerifier: OnlineCardVerifier = OnlineCardVerifier(),
+) {
+ private var backupCardsIds: Set = emptySet()
+
+ private val CardDTO.isBackupNotAllowed: Boolean
+ get() = !this.settings.isBackupAllowed
+
+ private val ScanResponse.userWalletName: String
+ get() = when (productType) {
+ ProductType.Note -> "Note"
+ ProductType.Twins -> "Twin"
+ ProductType.SaltPay -> "SaltPay"
+ ProductType.Wallet -> when {
+ card.isBackupNotAllowed -> "Tangem card"
+ card.isStart2Coin -> "Start2Coin"
+ else -> "Wallet"
+ }
+ }
+
+ private val ScanResponse.isMultiCurrency: Boolean
+ get() = when (productType) {
+ ProductType.Note -> false
+ ProductType.Twins -> false
+ ProductType.SaltPay -> false
+ ProductType.Wallet -> when {
+ card.isStart2Coin -> false
+ card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable -> true
+ else -> {
+ val cardWallets = card.wallets
+ require(cardWallets.isNotEmpty()) { "Card wallets must not be empty" }
+
+ cardWallets.first().curve == EllipticCurve.Secp256k1
+ }
+ }
+ }
+
+ fun backupCardsIds(backupCardsIds: Set?) = this.apply {
+ if (backupCardsIds != null) {
+ this.backupCardsIds = backupCardsIds
+ }
+ }
+
+ suspend fun build(): UserWallet? {
+ return with(scanResponse) {
+ UserWalletIdBuilder.scanResponse(scanResponse)
+ .build()
+ ?.let {
+ UserWallet(
+ walletId = it,
+ name = userWalletName,
+ artworkUrl = loadArtworkUrl(card.cardId, card.cardPublicKey),
+ cardsInWallet = backupCardsIds.plus(card.cardId),
+ scanResponse = this,
+ isMultiCurrency = isMultiCurrency,
+ )
+ }
+ }
+ }
+
+ private suspend fun loadArtworkUrl(cardId: String, cardPublicKey: ByteArray): String {
+ return when (val result = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) {
+ is Result.Success -> {
+ val artworkId = result.data.artwork?.id
+ if (artworkId.isNullOrEmpty()) {
+ getFallbackArtworkUrl(cardId)
+ } else {
+ getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
+ }
+ }
+
+ is Result.Failure -> getFallbackArtworkUrl(cardId)
+ }
+ }
+
+ private fun getFallbackArtworkUrl(cardId: String): String {
+ return when {
+ cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
+ cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
+ else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
+ TwinCardNumber.First -> Artwork.TWIN_CARD_1
+ TwinCardNumber.Second -> Artwork.TWIN_CARD_2
+ else -> Artwork.DEFAULT_IMG_URL
+ }
+ }
+ }
+
+ private fun getUrlForArtwork(cardId: String, cardPublicKeyHex: String, artworkId: String): String {
+ return TangemApi.Companion.BaseUrl.VERIFY.url + TangemApi.ARTWORK +
+ "?artworkId=${artworkId}&CID=${cardId}&publicKey=$cardPublicKeyHex"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt
new file mode 100644
index 0000000000..1fdef7c37c
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt
@@ -0,0 +1,70 @@
+package com.tangem.tap.domain.model.builders
+
+import com.tangem.common.extensions.calculateSha256
+import com.tangem.common.extensions.hexToBytes
+import com.tangem.crypto.Secp256k1
+import com.tangem.domain.common.CardDTO
+import com.tangem.domain.common.ProductType
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.TapWorkarounds.isTangemTwins
+import com.tangem.domain.common.extensions.calculateHmacSha256
+import com.tangem.domain.common.util.UserWalletId
+
+class UserWalletIdBuilder private constructor(
+ private val publicKey: ByteArray?,
+ private val pairTwinPublicKey: ByteArray? = null,
+) {
+ fun build(): UserWalletId? {
+ val seed = if (publicKey != null) {
+ if (pairTwinPublicKey != null) {
+ Secp256k1.sum(publicKey, pairTwinPublicKey)
+ } else {
+ publicKey
+ }
+ } else null
+
+ return seed?.let {
+ UserWalletId(value = calculateUserWalletId(it))
+ }
+ }
+
+ private fun calculateUserWalletId(seed: ByteArray?): ByteArray? {
+ val message = MESSAGE_FOR_WALLET_ID.toByteArray()
+ val keyHash = seed?.calculateSha256()
+
+ return if (keyHash != null) {
+ message.calculateHmacSha256(keyHash)
+ } else null
+ }
+
+ companion object {
+ private const val MESSAGE_FOR_WALLET_ID = "UserWalletID"
+
+ @Throws(IllegalArgumentException::class)
+ fun card(card: CardDTO): UserWalletIdBuilder {
+ require(!card.isTangemTwins) {
+ "For twin cards use scanResponse to ID calculation"
+ }
+
+ return UserWalletIdBuilder(findPublicKey(card.wallets))
+ }
+
+ fun scanResponse(scanResponse: ScanResponse): UserWalletIdBuilder {
+ return UserWalletIdBuilder(
+ publicKey = findPublicKey(scanResponse.card.wallets),
+ pairTwinPublicKey = when (scanResponse.productType) {
+ ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes()
+ ProductType.Note,
+ ProductType.Wallet,
+ ProductType.SaltPay,
+ -> null
+ },
+ )
+ }
+
+ private fun findPublicKey(wallets: List): ByteArray? {
+ return wallets.firstOrNull()
+ ?.publicKey
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
new file mode 100644
index 0000000000..e76a291668
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
@@ -0,0 +1,151 @@
+package com.tangem.tap.domain.model.builders
+
+import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.Token
+import com.tangem.blockchain.common.WalletManager
+import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.WalletDataModel
+import com.tangem.tap.domain.model.WalletStoreModel
+import com.tangem.tap.domain.tokens.models.BlockchainNetwork
+import com.tangem.tap.features.wallet.models.Currency
+import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
+import java.math.BigDecimal
+
+interface WalletStoreBuilder {
+ fun build(): WalletStoreModel
+
+ interface BlockchainNetworkWalletStoreBuilder : WalletStoreBuilder {
+ fun walletManager(walletManager: WalletManager?): WalletStoreBuilder
+ }
+
+ interface WalletMangerWalletStoreBuilder : WalletStoreBuilder
+
+ companion object {
+ operator fun invoke(
+ userWalletId: UserWalletId,
+ blockchainNetwork: BlockchainNetwork,
+ ): BlockchainNetworkWalletStoreBuilder {
+ return BlockchainNetworkWalletStoreBuilderImpl(userWalletId, blockchainNetwork)
+ }
+
+ operator fun invoke(
+ userWalletId: UserWalletId,
+ walletManager: WalletManager,
+ ): WalletMangerWalletStoreBuilder {
+ return WalletMangerWalletStoreBuilderImpl(userWalletId, walletManager)
+ }
+ }
+}
+
+private class BlockchainNetworkWalletStoreBuilderImpl(
+ private val userWalletId: UserWalletId,
+ private val blockchainNetwork: BlockchainNetwork,
+) : WalletStoreBuilder.BlockchainNetworkWalletStoreBuilder {
+ private var walletManager: WalletManager? = null
+
+ override fun walletManager(walletManager: WalletManager?) = this.apply {
+ this.walletManager = walletManager
+ }
+
+ override fun build(): WalletStoreModel {
+ val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager)
+ val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
+
+ return WalletStoreModel(
+ userWalletId = userWalletId,
+ blockchain = blockchainNetwork.blockchain,
+ derivationPath = blockchainNetwork.derivationPath?.let { DerivationPath(it) },
+ walletsData = (listOf(blockchainWalletData) + tokensWalletsData),
+ walletRent = null,
+ walletManager = walletManager,
+ blockchainNetwork = blockchainNetwork,
+ )
+ }
+}
+
+private class WalletMangerWalletStoreBuilderImpl(
+ private val userWalletId: UserWalletId,
+ private val walletManager: WalletManager,
+) : WalletStoreBuilder.WalletMangerWalletStoreBuilder {
+
+ override fun build(): WalletStoreModel {
+ val wallet = walletManager.wallet
+ val blockchainWalletData = wallet.blockchain.toBlockchainWalletData(walletManager)
+ val tokenWalletsData = wallet.getTokens().firstOrNull()?.toTokenWalletData(walletManager)
+
+ return WalletStoreModel(
+ userWalletId = userWalletId,
+ blockchain = wallet.blockchain,
+ derivationPath = wallet.publicKey.derivationPath,
+ walletsData = (listOf(blockchainWalletData) + listOfNotNull(tokenWalletsData)),
+ walletRent = null,
+ walletManager = walletManager,
+ blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager),
+ )
+ }
+}
+
+private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManager?): WalletDataModel {
+ return WalletDataModel(
+ currency = Currency.Blockchain(
+ blockchain = blockchain,
+ derivationPath = derivationPath,
+ ),
+ status = WalletDataModel.Loading,
+ walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
+ existentialDeposit = getExistentialDeposit(walletManager),
+ fiatRate = null,
+ )
+}
+
+private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?): List {
+ return this.tokens
+ .map { token ->
+ WalletDataModel(
+ currency = Currency.Token(
+ token = token,
+ blockchain = blockchain,
+ derivationPath = derivationPath,
+ ),
+ status = WalletDataModel.Loading,
+ walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
+ existentialDeposit = getExistentialDeposit(walletManager),
+ fiatRate = null,
+ )
+ }
+}
+
+private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): WalletDataModel {
+ val wallet = walletManager.wallet
+ return WalletDataModel(
+ currency = Currency.Blockchain(
+ blockchain = this,
+ derivationPath = wallet.publicKey.derivationPath?.rawPath,
+ ),
+ status = WalletDataModel.Loading,
+ walletAddresses = wallet.createAddressesData(),
+ existentialDeposit = getExistentialDeposit(walletManager),
+ fiatRate = null,
+ )
+}
+
+private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataModel {
+ val wallet = walletManager.wallet
+ return WalletDataModel(
+ currency = Currency.Token(
+ token = this,
+ blockchain = wallet.blockchain,
+ derivationPath = wallet.publicKey.derivationPath?.rawPath,
+ ),
+ status = WalletDataModel.Loading,
+ walletAddresses = wallet.createAddressesData(),
+ existentialDeposit = getExistentialDeposit(walletManager),
+ fiatRate = null,
+ )
+}
+
+private fun getExistentialDeposit(walletManager: WalletManager?): BigDecimal? {
+ return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
new file mode 100644
index 0000000000..d940a338ad
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
@@ -0,0 +1,250 @@
+package com.tangem.tap.domain.scanCard
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.common.core.TangemError
+import com.tangem.common.core.TangemSdkError
+import com.tangem.common.doOnFailure
+import com.tangem.common.doOnSuccess
+import com.tangem.common.services.Result
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.operations.backup.BackupService
+import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
+import com.tangem.tap.backupService
+import com.tangem.tap.common.analytics.Analytics
+import com.tangem.tap.common.analytics.events.IntroductionProcess
+import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
+import com.tangem.tap.common.extensions.dispatchDialogShow
+import com.tangem.tap.common.extensions.dispatchOnMain
+import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
+import com.tangem.tap.common.redux.AppDialog
+import com.tangem.tap.common.redux.global.GlobalAction
+import com.tangem.tap.common.redux.navigation.AppScreen
+import com.tangem.tap.common.redux.navigation.NavigationAction
+import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
+import com.tangem.tap.features.disclaimer.redux.DisclaimerType
+import com.tangem.tap.features.disclaimer.redux.isAccepted
+import com.tangem.tap.features.onboarding.OnboardingHelper
+import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
+import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
+import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
+import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
+import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
+import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
+import com.tangem.tap.preferencesStorage
+import com.tangem.tap.scope
+import com.tangem.tap.store
+import com.tangem.tap.tangemSdkManager
+import com.tangem.tap.userTokensRepository
+import com.tangem.wallet.R
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+// TODO: Create repository for that
+object ScanCardProcessor {
+ suspend fun scan(
+ additionalBlockchainsToDerive: Collection? = null,
+ cardId: String? = null,
+ onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
+ onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {},
+ onWalletNotCreated: suspend (() -> Unit) = {},
+ onFailure: suspend (error: TangemError) -> Unit = {},
+ onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {},
+ ) = withMainContext {
+ onProgressStateChange(true)
+ onScanStateChange(true)
+
+ tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
+
+ val result = tangemSdkManager.scanProduct(
+ userTokensRepository = userTokensRepository,
+ cardId = cardId,
+ additionalBlockchainsToDerive = additionalBlockchainsToDerive,
+ )
+
+ store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
+
+ result
+ .doOnFailure { error ->
+ onProgressStateChange(false)
+ onScanStateChange(false)
+ onFailure(error)
+ }
+ .doOnSuccess { scanResponse ->
+ tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
+
+ onScanStateChange(false)
+ checkForUnfinishedBackupForSaltPay(
+ backupService = backupService,
+ scanResponse = scanResponse,
+ onProgressStateChange = { onProgressStateChange(it) },
+ nextHandler = { scanResponse1 ->
+ showDisclaimerIfNeed(
+ scanResponse = scanResponse1,
+ onProgressStateChange = onProgressStateChange,
+ nextHandler = { scanResponse2 ->
+ onScanSuccess(
+ scanResponse = scanResponse2,
+ onProgressStateChange = onProgressStateChange,
+ onSuccess = onSuccess,
+ onWalletNotCreated = onWalletNotCreated,
+ onFailure = onFailure,
+ )
+ },
+ )
+ },
+ )
+ }
+ }
+
+ /**
+ * It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
+ * see BackupAction.CheckForUnfinishedBackup
+ * If user touches card other than Visa SaltPay - show dialog and block next processing
+ */
+ private inline fun checkForUnfinishedBackupForSaltPay(
+ backupService: BackupService,
+ scanResponse: ScanResponse,
+ onProgressStateChange: (showProgress: Boolean) -> Unit,
+ nextHandler: (ScanResponse) -> Unit,
+ ) {
+ if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
+ nextHandler(scanResponse)
+ return
+ }
+
+ val isTheSamePrimaryCard = backupService.primaryCardId
+ ?.let { it == scanResponse.card.cardId }
+ ?: false
+
+ if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard) {
+ onProgressStateChange(false)
+ showSaltPayTapVisaLogoCardDialog()
+ } else {
+ nextHandler(scanResponse)
+ }
+ }
+
+ private suspend inline fun showDisclaimerIfNeed(
+ scanResponse: ScanResponse,
+ crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
+ crossinline nextHandler: suspend (ScanResponse) -> Unit,
+ ) {
+ val disclaimerType = DisclaimerType.get(scanResponse)
+ store.dispatchOnMain(DisclaimerAction.SetDisclaimerType(disclaimerType))
+
+ if (disclaimerType.isAccepted()) {
+ nextHandler((scanResponse))
+ } else scope.launch(Dispatchers.Main) {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ store.dispatchOnMain(
+ DisclaimerAction.Show(
+ onAcceptCallback = {
+ scope.launch(Dispatchers.Main) {
+ nextHandler(scanResponse)
+ }
+ },
+ onDismissCallback = {
+ scope.launch(Dispatchers.Main) {
+ onProgressStateChange(false)
+ }
+ },
+ ),
+ )
+ }
+ }
+
+ private suspend inline fun onScanSuccess(
+ scanResponse: ScanResponse,
+ crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
+ crossinline onWalletNotCreated: suspend () -> Unit,
+ crossinline onSuccess: suspend (ScanResponse) -> Unit,
+ crossinline onFailure: suspend (error: TangemError) -> Unit,
+ ) {
+ Analytics.send(IntroductionProcess.CardWasScanned())
+
+ val globalState = store.state.globalState
+ val tapWalletManager = globalState.tapWalletManager
+ tapWalletManager.updateConfigManager(scanResponse)
+
+ Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId))
+
+ store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
+
+ if (scanResponse.isSaltPay()) {
+ if (scanResponse.isSaltPayVisa()) {
+ val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
+ val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
+ delay(500)
+ withMainContext {
+ when (result) {
+ is Result.Success -> {
+ val isOnboardingCase = result.data
+ if (isOnboardingCase) {
+ onWalletNotCreated()
+ store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
+ store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config))
+ store.dispatchOnMain(OnboardingSaltPayAction.Update)
+ navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) }
+ } else {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ onSuccess(scanResponse)
+ onProgressStateChange(false)
+ }
+ }
+ is Result.Failure -> {
+ SaltPayExceptionHandler.handle(result.error)
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ onFailure(TangemSdkError.ExceptionError(result.error))
+ onProgressStateChange(false)
+ }
+ }
+ }
+ } else {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ if (scanResponse.card.backupStatus?.isActive == false) {
+ showSaltPayTapVisaLogoCardDialog()
+ } else {
+ onSuccess(scanResponse)
+ }
+ onProgressStateChange(false)
+ }
+ } else {
+ if (OnboardingHelper.isOnboardingCase(scanResponse)) {
+ onWalletNotCreated()
+ store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
+ val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
+ navigateTo(appScreen) { onProgressStateChange(it) }
+ } else {
+ if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
+ onWalletNotCreated()
+ store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
+ navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
+ } else {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ onSuccess(scanResponse)
+ onProgressStateChange(false)
+ }
+ }
+ }
+ }
+
+ private fun showSaltPayTapVisaLogoCardDialog() {
+ store.dispatchDialogShow(
+ AppDialog.SimpleOkDialogRes(
+ headerId = R.string.saltpay_error_empty_backup_title,
+ messageId = R.string.saltpay_error_empty_backup_message,
+ ),
+ )
+ }
+
+ private suspend inline fun navigateTo(
+ screen: AppScreen,
+ onProgressStateChange: (showProgress: Boolean) -> Unit,
+ ) {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ store.dispatchOnMain(NavigationAction.NavigateTo(screen))
+ onProgressStateChange(false)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
index 982ed86857..98ce39259e 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
@@ -1,3 +1,5 @@
+package com.tangem.tap.domain.tasks.product
+
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
@@ -9,6 +11,8 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.common.map
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.KeyWalletPublicKey
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@@ -19,49 +23,72 @@ import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.ExtendedPublicKeysMap
-import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
-import com.tangem.tap.domain.tasks.product.CreateWalletsTask
-import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
-import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
import com.tangem.tap.features.demo.DemoHelper
-
+import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse
data class CreateProductWalletTaskResponse(
- val card: Card,
+ val card: CardDTO,
val derivedKeys: Map = mapOf(),
- val primaryCard: PrimaryCard? = null
-) : CommandResponse
+ val primaryCard: PrimaryCard? = null,
+) : CommandResponse {
+ constructor(
+ card: Card,
+ derivedKeys: Map = mapOf(),
+ primaryCard: PrimaryCard? = null,
+ ) : this(
+ card = CardDTO(card),
+ derivedKeys = derivedKeys,
+ primaryCard = primaryCard,
+ )
+}
+
+private data class CreateWalletResponse(
+ val cardId: String,
+ val wallet: CardDTO.Wallet,
+) {
+ constructor(
+ sdkResponse: SdkCreateWalletResponse,
+ ) : this(
+ cardId = sdkResponse.cardId,
+ wallet = CardDTO.Wallet(sdkResponse.wallet),
+ )
+}
class CreateProductWalletTask(
private val type: ProductType,
) : CardSessionRunnable {
+ override val allowsRequestAccessCodeFromRepository: Boolean = false
+
override fun run(
session: CardSession,
- callback: (result: CompletionResult) -> Unit
+ callback: (result: CompletionResult) -> Unit,
) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
+ val cardDto = CardDTO(card)
val commandProcessor = when (type) {
ProductType.Note -> CreateWalletTangemNote()
ProductType.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet()
}
- commandProcessor.proceed(card, session) {
+ commandProcessor.proceed(cardDto, session) {
when (it) {
is CompletionResult.Success -> {
val result = when (commandProcessor) {
is CreateWalletTangemWallet -> {
it.data as CreateProductWalletTaskResponse
}
+
else -> CreateProductWalletTaskResponse(card = session.environment.card!!)
}
callback(CompletionResult.Success(result))
}
+
is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error))
}
}
@@ -70,7 +97,7 @@ class CreateProductWalletTask(
private class CreateWalletTangemNote : ProductCommandProcessor {
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -94,52 +121,65 @@ private class CreateWalletTangemNote : ProductCommandProcessor
+ callback(result.map { CreateWalletResponse(it) })
+ }
}
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor {
- private lateinit var card: Card
private var primaryCard: PrimaryCard? = null
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
- this.card = card
val curves = card.getCurvesForNonCreatedWallets()
if (curves.isEmpty()) {
- val createWalletResponses = card.wallets.map { CreateWalletResponse(card.cardId, it) }
- proceedWithCreatedWallets(createWalletResponses, session, callback)
+ val createWalletResponses = card.wallets.map { wallet ->
+ CreateWalletResponse(card.cardId, wallet)
+ }
+ proceedWithCreatedWallets(card, createWalletResponses, session, callback)
return
}
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
- proceedWithCreatedWallets(result.data.createWalletResponses, session, callback)
+ proceedWithCreatedWallets(
+ card = card,
+ createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) },
+ session = session,
+ callback = callback,
+ )
+ }
+
+ is CompletionResult.Failure -> {
+ callback(CompletionResult.Failure(result.error))
}
- is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun proceedWithCreatedWallets(
+ card: CardDTO,
createWalletResponses: List,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
when {
card.settings.isBackupAllowed -> {
- linkPrimaryCard(createWalletResponses, session, callback)
+ linkPrimaryCard(card, createWalletResponses, session, callback)
}
+
card.settings.isHDWalletAllowed -> {
- deriveKeys(createWalletResponses, session, callback)
+ deriveKeys(card, createWalletResponses, session, callback)
}
+
else -> {
callback(
CompletionResult.Success(
@@ -151,6 +191,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
@@ -161,8 +202,9 @@ private class CreateWalletTangemWallet : ProductCommandProcessor {
- deriveKeys(createWalletResponse, session, callback)
+ deriveKeys(card, createWalletResponse, session, callback)
}
+
else -> {
callback(
CompletionResult.Success(
@@ -174,6 +216,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor {
callback(CompletionResult.Failure(result.error))
}
@@ -182,13 +225,14 @@ private class CreateWalletTangemWallet : ProductCommandProcessor,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
val map = mutableMapOf>()
createWalletResponse.forEach { response ->
- val blockchainsForCurve = getBlockchains(response.cardId).filter {
+ val blockchainsForCurve = getBlockchains(response.cardId, card).filter {
it.getSupportedCurves().contains(response.wallet.curve)
}
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath(card.derivationStyle) }
@@ -210,17 +254,21 @@ private class CreateWalletTangemWallet : ProductCommandProcessor callback(CompletionResult.Failure(result.error))
}
}
}
- private fun getBlockchains(cardId: String): List {
+ private fun getBlockchains(
+ cardId: String,
+ card: CardDTO,
+ ): List {
return when {
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
index c0810ff887..8b9d8d9d2f 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
@@ -1,15 +1,15 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
-import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
+import com.tangem.domain.common.CardDTO
/**
[REDACTED_AUTHOR]
*/
interface ProductCommandProcessor {
fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
)
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 d028eb9a9e..b04226d1d6 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
@@ -13,6 +13,7 @@ import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isExcluded
@@ -32,7 +33,6 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.extensions.getPrimaryCurve
-import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
@@ -46,6 +46,9 @@ class ScanProductTask(
private val additionalBlockchainsToDerive: Collection? = null,
) : CardSessionRunnable {
+ override val allowsRequestAccessCodeFromRepository: Boolean
+ get() = !additionalBlockchainsToDerive.isNullOrEmpty()
+
override fun run(
session: CardSession,
callback: (result: CompletionResult) -> Unit,
@@ -54,19 +57,20 @@ class ScanProductTask(
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
+ val cardDto = CardDTO(card)
- val error = getErrorIfExcludedCard(card)
+ val error = getErrorIfExcludedCard(cardDto)
if (error != null) {
callback(CompletionResult.Failure(error))
return
}
val commandProcessor = when {
- card.isTangemNote -> ScanNoteProcessor()
- card.isTangemTwins -> ScanTwinProcessor()
+ cardDto.isTangemNote -> ScanNoteProcessor()
+ cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive)
}
- commandProcessor.proceed(card, session) { processorResult ->
+ commandProcessor.proceed(cardDto, session) { processorResult ->
when (processorResult) {
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
@@ -74,7 +78,7 @@ class ScanProductTask(
// it need because processorResult.data.card doesn't contains attestation result
// and CardWallet.derivedKeys
val processorScanResponseWithNewCard = processorResult.data.copy(
- card = scanTaskResult.data,
+ card = CardDTO(scanTaskResult.data),
)
callback(CompletionResult.Success(processorScanResponseWithNewCard))
}
@@ -86,7 +90,7 @@ class ScanProductTask(
}
}
- private fun getErrorIfExcludedCard(card: Card): TangemError? {
+ private fun getErrorIfExcludedCard(card: CardDTO): TangemError? {
if (card.isExcluded) return TapSdkError.CardForDifferentApp
if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease
return null
@@ -95,7 +99,7 @@ class ScanProductTask(
private class ScanNoteProcessor : ProductCommandProcessor {
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -118,7 +122,7 @@ private class ScanWalletProcessor(
var primaryCard: PrimaryCard? = null
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -126,7 +130,7 @@ private class ScanWalletProcessor(
}
private fun createMissingWalletsIfNeeded(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -160,13 +164,15 @@ private class ScanWalletProcessor(
}
private fun startLinkingForBackupIfNeeded(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
- val activationIsFinished = preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId)
+ val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
- if (card.backupStatus == Card.BackupStatus.NoBackup && !activationIsFinished && card.wallets.isNotEmpty()) {
+ if ((card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty())
+ && (activationInProgress || card.isSaltPay)
+ ) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
@@ -184,7 +190,7 @@ private class ScanWalletProcessor(
}
private fun deriveKeysIfNeeded(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -227,25 +233,41 @@ private class ScanWalletProcessor(
}
}
- private suspend fun getBlockchainsToDerive(card: Card): List {
+ private suspend fun getBlockchainsToDerive(card: CardDTO): List {
val userTokensRepository = userTokensRepository ?: return emptyList()
- val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty {
- mutableListOf(
- BlockchainNetwork(Blockchain.Bitcoin, card),
- BlockchainNetwork(Blockchain.Ethereum, card),
- )
- }
+ val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card)
+ .toMutableList()
+ .ifEmpty {
+ mutableListOf(
+ BlockchainNetwork(
+ blockchain = Blockchain.Bitcoin,
+ card = card,
+ ),
+ BlockchainNetwork(
+ blockchain = Blockchain.Ethereum,
+ card = card,
+ ),
+ )
+ }
if (card.settings.isHDWalletAllowed) {
blockchainsToDerive.addAll(
listOf(
- BlockchainNetwork(Blockchain.Ethereum, card),
- BlockchainNetwork(Blockchain.EthereumTestnet, card),
+ BlockchainNetwork(
+ blockchain = Blockchain.Ethereum,
+ card = card,
+ ),
+ BlockchainNetwork(
+ blockchain = Blockchain.EthereumTestnet,
+ card = card,
+ ),
),
)
}
if (additionalBlockchainsToDerive != null) {
- blockchainsToDerive.addAll(additionalBlockchainsToDerive.map { BlockchainNetwork(it, card) })
+ blockchainsToDerive.addAll(
+ additionalBlockchainsToDerive.map { BlockchainNetwork(it, card) }
+ )
}
if (!card.useOldStyleDerivation) {
blockchainsToDerive.removeAll(
@@ -261,7 +283,7 @@ private class ScanWalletProcessor(
return blockchainsToDerive.distinct()
}
- private suspend fun collectDerivations(card: Card): Map> {
+ private suspend fun collectDerivations(card: CardDTO): Map> {
val blockchains = getBlockchainsToDerive(card)
val derivations = mutableMapOf>()
@@ -287,14 +309,14 @@ private class ScanWalletProcessor(
private class ScanTwinProcessor : ProductCommandProcessor {
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
- val publicKey = card.getSingleWallet()?.publicKey
+ val publicKey = card.wallets.firstOrNull()?.publicKey
if (publicKey == null) {
val response = ScanResponse(
card = card,
@@ -332,7 +354,7 @@ private class ScanTwinProcessor : ProductCommandProcessor {
}
}
-fun Card.getCurvesForNonCreatedWallets(): List {
+fun CardDTO.getCurvesForNonCreatedWallets(): List {
val curvesPresent = wallets.map { it.curve }.toSet()
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
return curvesForNonCreatedWallets.toList()
diff --git a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt
index f05065bc8d..6644362bf5 100644
--- a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt
+++ b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt
@@ -3,8 +3,8 @@ package com.tangem.tap.domain.termsOfUse
import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat
-import com.tangem.common.card.Card
-import java.util.Locale
+import com.tangem.domain.common.CardDTO
+import java.util.*
/**
[REDACTED_AUTHOR]
@@ -13,7 +13,7 @@ class CardTou {
private val locale: Locale =
ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)!!
- fun getUrl(card: Card): Uri? {
+ fun getUrl(card: CardDTO): Uri? {
val issuerName = card.issuer.name
if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
index d3d5836701..28bcdc9c35 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
@@ -2,10 +2,11 @@ package com.tangem.tap.domain.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.FirmwareVersion
+import com.tangem.domain.common.CardDTO
object CurrenciesRepository {
fun getBlockchains(
- cardFirmware: FirmwareVersion,
+ cardFirmware: CardDTO.FirmwareVersion,
isTestNet: Boolean = false,
): List {
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
@@ -26,6 +27,4 @@ object CurrenciesRepository {
)
}
}
-}
-
-
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt
index 6e94b5a88a..0e18c4018d 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt
@@ -3,7 +3,7 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
-import com.tangem.network.api.tangemTech.CoinsResponse
+import com.tangem.datasource.api.tangemTech.CoinsResponse
@JsonClass(generateAdapter = true)
data class CurrencyFromJson(
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt
index 7df68e80c3..fbbaad9022 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt
@@ -6,9 +6,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getListOfCoins
import com.tangem.domain.common.extensions.toNetworkId
-import com.tangem.network.api.tangemTech.CoinsResponse
-import com.tangem.network.api.tangemTech.TangemTechService
-import com.tangem.network.common.MoshiConverter
+import com.tangem.datasource.api.tangemTech.CoinsResponse
+import com.tangem.datasource.api.tangemTech.TangemTechService
+import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.common.AssetReader
class LoadAvailableCoinsService(
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt
index 7418891662..2aa1dce1a7 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt
@@ -7,8 +7,8 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
-import com.tangem.network.api.tangemTech.TangemTechService
-import com.tangem.network.common.MoshiConverter
+import com.tangem.datasource.api.tangemTech.TangemTechService
+import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.TokenDao
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt
index c0b7e18155..821a8046ac 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt
@@ -2,8 +2,8 @@ package com.tangem.tap.domain.tokens
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
-import com.tangem.network.api.tangemTech.TangemTechService
-import com.tangem.network.api.tangemTech.UserTokensResponse
+import com.tangem.datasource.api.tangemTech.TangemTechService
+import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.tap.domain.NoDataError
class UserTokensNetworkService(private val tangemTechService: TangemTechService) {
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
index 40b8389049..b3efb6ce5b 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
@@ -2,13 +2,13 @@ package com.tangem.tap.domain.tokens
import android.content.Context
import com.tangem.blockchain.common.DerivationStyle
-import com.tangem.common.card.Card
import com.tangem.common.services.Result
-import com.tangem.network.api.tangemTech.TangemTechService
-import com.tangem.network.api.tangemTech.UserTokensResponse
+import com.tangem.datasource.api.tangemTech.TangemTechService
+import com.tangem.datasource.api.tangemTech.UserTokensResponse
+import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
-import com.tangem.tap.domain.extensions.getUserWalletId
+import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
@@ -23,8 +23,8 @@ class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val networkService: UserTokensNetworkService,
) {
- suspend fun getUserTokens(card: Card): List {
- val userId = card.getUserWalletId()
+ suspend fun getUserTokens(card: CardDTO): List {
+ val userId = getUserWalletId(card) ?: return emptyList()
if (DemoHelper.isDemoCardId(card.cardId)) {
return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() }
}
@@ -36,25 +36,28 @@ class UserTokensRepository(
return when (val networkResult = networkService.getUserTokens(userId)) {
is Result.Success -> {
val tokens = networkResult.data.tokens.mapNotNull { Currency.fromTokenResponse(it) }
- storageService.saveUserTokens(card.getUserWalletId(), tokens.toUserTokensResponse())
+ storageService.saveUserTokens(userId, tokens.toUserTokensResponse())
tokens.distinct()
}
+
is Result.Failure -> {
handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error)
}
}
}
- suspend fun saveUserTokens(card: Card, tokens: List) {
+ suspend fun saveUserTokens(card: CardDTO, tokens: List) {
+ val userId = getUserWalletId(card) ?: return
val userTokens = tokens.toUserTokensResponse()
- networkService.saveUserTokens(card.getUserWalletId(), userTokens)
- storageService.saveUserTokens(card.getUserWalletId(), userTokens)
+ networkService.saveUserTokens(userId, userTokens)
+ storageService.saveUserTokens(userId, userTokens)
}
- suspend fun removeUserTokens(card: Card) {
+ suspend fun removeUserTokens(card: CardDTO) {
+ val userId = getUserWalletId(card) ?: return
val userTokens = emptyList().toUserTokensResponse()
- networkService.saveUserTokens(card.getUserWalletId(), userTokens)
- storageService.saveUserTokens(card.getUserWalletId(), userTokens)
+ networkService.saveUserTokens(userId, userTokens)
+ storageService.saveUserTokens(userId, userTokens)
}
private fun List.toUserTokensResponse(): UserTokensResponse {
@@ -66,8 +69,8 @@ class UserTokensRepository(
)
}
- suspend fun loadBlockchainsToDerive(card: Card): List {
- val userId = card.getUserWalletId()
+ suspend fun loadBlockchainsToDerive(card: CardDTO): List {
+ val userId = getUserWalletId(card) ?: return emptyList()
val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) {
@@ -89,7 +92,7 @@ class UserTokensRepository(
}
private suspend fun handleGetUserTokensFailure(
- card: Card,
+ card: CardDTO,
userId: String,
error: Throwable,
): List {
@@ -100,6 +103,7 @@ class UserTokensRepository(
coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = userTokens) } }
tokens
}
+
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
tokens.distinct()
@@ -107,10 +111,15 @@ class UserTokensRepository(
}
}
- private suspend fun loadTokensOffline(card: Card, userId: String): List {
+ private suspend fun loadTokensOffline(card: CardDTO, userId: String): List {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
+ private fun getUserWalletId(card: CardDTO): String? {
+ return UserWalletIdBuilder.card(card).build()
+ ?.stringValue
+ }
+
companion object {
const val SORT_DEFAULT_VALUE = "manual"
const val GROUP_DEFAULT_VALUE = "none"
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt
index c71e5afa53..9e5dac0b68 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt
@@ -2,9 +2,9 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
-import com.tangem.common.card.Card
-import com.tangem.network.api.tangemTech.UserTokensResponse
-import com.tangem.network.common.MoshiConverter
+import com.tangem.datasource.api.tangemTech.UserTokensResponse
+import com.tangem.datasource.api.common.MoshiConverter
+import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
@@ -28,7 +28,7 @@ class UserTokensStorageService(
}
@Deprecated("")
- suspend fun getUserTokens(card: Card): List {
+ suspend fun getUserTokens(card: CardDTO): List {
val blockchainNetworks =
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
return blockchainNetworks.flatMap { it.toCurrencies() }
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
index 36e376a08b..3b9558c303 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
@@ -4,8 +4,8 @@ import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
-import com.tangem.common.card.Card
import com.tangem.common.extensions.calculateHashCode
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@JsonClass(generateAdapter = true)
@@ -15,16 +15,18 @@ data class BlockchainNetwork(
val tokens: List
) {
- constructor(blockchain: Blockchain, card: Card) : this(
+ constructor(
+ blockchain: Blockchain,
+ card: CardDTO,
+ ) : this(
blockchain = blockchain,
derivationPath = if (card.settings.isHDWalletAllowed) blockchain.derivationPath(card.derivationStyle)?.rawPath else null,
- tokens = emptyList()
+ tokens = emptyList(),
)
-
fun updateTokens(tokens: List): BlockchainNetwork {
return copy(
- tokens = (this.tokens + tokens).distinct()
+ tokens = (this.tokens + tokens).distinct(),
)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt
new file mode 100644
index 0000000000..9790310eb0
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt
@@ -0,0 +1,34 @@
+package com.tangem.tap.domain.totalBalance
+
+import com.tangem.tap.domain.model.TotalFiatBalance
+import com.tangem.tap.domain.model.WalletStoreModel
+import java.math.BigDecimal
+
+interface TotalFiatBalanceCalculator {
+
+ /**
+ * Calculate total fiat balance for list of [WalletStoreModel]
+ * @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing]
+ * @param walletStores List of [WalletStoreModel] to calculate fiat amount
+ * @param initial Initial [TotalFiatBalance] state, used when list of [WalletStoreModel] is empty
+ * @return [TotalFiatBalance] with state found with the [WalletStoreModel] list
+ * */
+ suspend fun calculate(
+ prevAmount: BigDecimal?,
+ walletStores: List,
+ initial: TotalFiatBalance,
+ ): TotalFiatBalance
+
+ /**
+ * Same as [TotalFiatBalanceCalculator.calculate] but returns null if list of [WalletStoreModel] is empty
+ * @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing]
+ * @param walletStores List of [WalletStoreModel] to calculate fiat amount
+ * @return [TotalFiatBalance] with state found with the [WalletStoreModel] list
+ * */
+ suspend fun calculateOrNull(
+ prevAmount: BigDecimal?,
+ walletStores: List,
+ ): TotalFiatBalance?
+
+ companion object
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt
new file mode 100644
index 0000000000..ea285cc404
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt
@@ -0,0 +1,8 @@
+package com.tangem.tap.domain.totalBalance.di
+
+import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
+import com.tangem.tap.domain.totalBalance.implementation.DefaultTotalFiatBalanceCalculator
+
+fun TotalFiatBalanceCalculator.Companion.provideDefaultImplementation(): TotalFiatBalanceCalculator {
+ return DefaultTotalFiatBalanceCalculator()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt
new file mode 100644
index 0000000000..0c0f909c80
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt
@@ -0,0 +1,99 @@
+package com.tangem.tap.domain.totalBalance.implementation
+
+import com.tangem.tap.common.extensions.toFiatValue
+import com.tangem.tap.domain.model.TotalFiatBalance
+import com.tangem.tap.domain.model.WalletDataModel
+import com.tangem.tap.domain.model.WalletStoreModel
+import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.math.BigDecimal
+
+internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
+ override suspend fun calculate(
+ prevAmount: BigDecimal?,
+ walletStores: List,
+ initial: TotalFiatBalance,
+ ): TotalFiatBalance {
+ return calculateOrNull(prevAmount, walletStores) ?: initial
+ }
+
+ override suspend fun calculateOrNull(
+ prevAmount: BigDecimal?,
+ walletStores: List,
+ ): TotalFiatBalance? {
+ return if (walletStores.isEmpty()) {
+ null
+ } else {
+ withContext(Dispatchers.Default) {
+ val walletsData = walletStores
+ .asSequence()
+ .flatMap { it.walletsData }
+ val calculateAmount = { walletsData.calculateTotalFiatAmount() }
+
+ when (walletsData.findStatus()) {
+ TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading
+ TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount())
+ TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount())
+ }
+ }
+ }
+ }
+
+ private fun Sequence.findStatus(): TotalFiatBalanceStatus {
+ return this
+ .mapToStatus()
+ .reduce { prevStatus, newStatus ->
+ getCurrentStatus(prevStatus, newStatus)
+ }
+ }
+
+ private fun Sequence.mapToStatus(): Sequence {
+ return this.map { walletData ->
+ when (walletData.status) {
+ is WalletDataModel.VerifiedOnline,
+ is WalletDataModel.SameCurrencyTransactionInProgress,
+ is WalletDataModel.TransactionInProgress,
+ is WalletDataModel.NoAccount,
+ -> TotalFiatBalanceStatus.Loaded
+ is WalletDataModel.Unreachable,
+ is WalletDataModel.MissedDerivation,
+ -> TotalFiatBalanceStatus.Error
+ is WalletDataModel.Loading -> TotalFiatBalanceStatus.Loading
+ }
+ }
+ }
+
+ private fun Sequence.calculateTotalFiatAmount(): BigDecimal {
+ return this
+ .map { walletData ->
+ walletData.fiatRate
+ ?.let { walletData.status.amount.toFiatValue(it) }
+ ?: BigDecimal.ZERO
+ }
+ .reduce(BigDecimal::plus)
+ }
+
+ private fun getCurrentStatus(
+ prevStatus: TotalFiatBalanceStatus,
+ newStatus: TotalFiatBalanceStatus,
+ ): TotalFiatBalanceStatus {
+ return when (prevStatus) {
+ TotalFiatBalanceStatus.Loading -> prevStatus
+ TotalFiatBalanceStatus.Loaded,
+ TotalFiatBalanceStatus.Error,
+ -> when (newStatus) {
+ TotalFiatBalanceStatus.Loading,
+ TotalFiatBalanceStatus.Error,
+ -> newStatus
+ TotalFiatBalanceStatus.Loaded -> prevStatus
+ }
+ }
+ }
+
+ private enum class TotalFiatBalanceStatus {
+ Loading,
+ Error,
+ Loaded,
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt
index 872c130c30..28016d704f 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt
@@ -4,18 +4,30 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
+import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
-import com.tangem.tap.domain.extensions.getSingleWallet
-class CreateFirstTwinWalletTask : CardSessionRunnable {
+class CreateFirstTwinWalletTask(private val firstCardId: String) : CardSessionRunnable {
+
+ override val allowsRequestAccessCodeFromRepository: Boolean = false
+
override fun run(
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
- val publicKey = session.environment.card?.getSingleWallet()?.publicKey
+ val card = session.environment.card
+ val publicKey = card?.wallets?.firstOrNull()?.publicKey
if (publicKey != null) {
+ val requiredTwinCardNumber = TwinsHelper.getTwinCardNumber(firstCardId)
+ if (requiredTwinCardNumber != TwinsHelper.getTwinCardNumber(card.cardId)) {
+ requiredTwinCardNumber?.let {
+ callback(CompletionResult.Failure(WrongTwinCard(it)))
+ }
+ return
+ }
+
PurgeWalletCommand(publicKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt
index 85c03925ff..628c041112 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt
@@ -6,28 +6,31 @@ import com.tangem.common.KeyPair
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
-import com.tangem.common.core.TangemError
import com.tangem.common.extensions.hexToBytes
import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
-import com.tangem.tap.domain.extensions.getSingleWallet
-import com.tangem.wallet.R
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
private val firstCardId: String,
private val issuerKeys: KeyPair,
private val preparingMessage: Message,
- private val creatingWalletMessage: Message
+ private val creatingWalletMessage: Message,
) : CardSessionRunnable {
+
+ override val allowsRequestAccessCodeFromRepository: Boolean = false
+
override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) {
val card = session.environment.card
- val publicKey = card?.getSingleWallet()?.publicKey
+ val publicKey = card?.wallets?.firstOrNull()?.publicKey
if (publicKey != null) {
- if (!card.cardId.startsWith(TwinsHelper.getPairCardSeries(firstCardId) ?: "")) {
- callback(CompletionResult.Failure(WrongTwinCard()))
+ val currentTwinCardNumber = TwinsHelper.getTwinCardNumber(card.cardId)
+ if (TwinsHelper.getTwinCardNumber(firstCardId) == currentTwinCardNumber) {
+ currentTwinCardNumber?.pairNumber()?.let {
+ callback(CompletionResult.Failure(WrongTwinCard(it)))
+ }
return
}
@@ -53,13 +56,10 @@ class CreateSecondTwinWalletTask(
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.updateWallet(result.data.wallet)
- WriteProtectedIssuerDataTask(
- firstPublicKey.hexToBytes(), issuerKeys
- ).run(session) { writeResult ->
+ WriteProtectedIssuerDataTask(firstPublicKey.hexToBytes(), issuerKeys).run(session) { writeResult ->
when (writeResult) {
is CompletionResult.Success -> callback(result)
- is CompletionResult.Failure ->
- callback(CompletionResult.Failure(writeResult.error))
+ is CompletionResult.Failure -> callback(CompletionResult.Failure(writeResult.error))
}
}
}
@@ -67,10 +67,4 @@ class CreateSecondTwinWalletTask(
}
}
}
-
- private class WrongTwinCard : TangemError {
- override val code: Int = 50005
- override var customMessage: String = code.toString()
- override val messageResId = R.string.twins_wrong_card_error
- }
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
index 73b9e2f40a..d4e00cb3fe 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
@@ -13,6 +13,8 @@ class FinalizeTwinTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair,
) : CardSessionRunnable {
+ override val allowsRequestAccessCodeFromRepository: Boolean = false
+
override fun run(
session: CardSession,
callback: (result: CompletionResult) -> Unit,
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
index feb58ca6f0..b910fcb589 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
@@ -6,25 +6,20 @@ import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
-import com.tangem.common.card.Card
-import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
+import com.tangem.datasource.api.common.MoshiConverter
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
-import com.tangem.network.common.MoshiConverter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.AssetReader
-import com.tangem.tap.common.analytics.AnalyticsAnOld
-import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(
- private val card: Card,
+ card: CardDTO,
assetReader: AssetReader,
- val analyticsHandler: GlobalAnalyticsEventHandler?
) {
-
- private val currentCardId: String = card.cardId
+ private val firstCardId: String = card.cardId
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
@@ -32,18 +27,14 @@ class TwinCardsManager(
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
suspend fun createFirstWallet(message: Message): CompletionResult {
- val response = tangemSdkManager.runTaskAsync(CreateFirstTwinWalletTask(), currentCardId, message)
+ val response = tangemSdkManager.runTaskAsync(
+ runnable = CreateFirstTwinWalletTask(firstCardId),
+ cardId = firstCardId,
+ initialMessage = message,
+ )
when (response) {
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
- is CompletionResult.Failure -> {
- (response.error as? TangemSdkError)?.let { error ->
- analyticsHandler?.handleCardSdkErrorEvent(
- error,
- AnalyticsAnOld.ActionToLog.CreateWallet,
- card = card
- )
- }
- }
+ is CompletionResult.Failure -> {}
}
return response
}
@@ -55,46 +46,30 @@ class TwinCardsManager(
): CompletionResult {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
- firstCardId = currentCardId,
+ firstCardId = firstCardId,
issuerKeys = issuerKeyPair,
preparingMessage = preparingMessage,
- creatingWalletMessage = creatingWalletMessage
+ creatingWalletMessage = creatingWalletMessage,
)
val response = tangemSdkManager.runTaskAsync(task, null, initialMessage)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
}
- is CompletionResult.Failure -> {
- (response.error as? TangemSdkError)?.let { error ->
- analyticsHandler?.handleCardSdkErrorEvent(
- error,
- AnalyticsAnOld.ActionToLog.CreateWallet,
- card = card
- )
- }
- }
+ is CompletionResult.Failure -> {}
}
return response
}
suspend fun complete(message: Message): Result {
val response = tangemSdkManager.runTaskAsync(
- FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
- currentCardId, message
+ runnable = FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
+ cardId = firstCardId,
+ initialMessage = message,
)
return when (response) {
is CompletionResult.Success -> Result.Success(response.data)
- is CompletionResult.Failure -> {
- (response.error as? TangemSdkError)?.let { error ->
- analyticsHandler?.handleCardSdkErrorEvent(
- error,
- AnalyticsAnOld.ActionToLog.WriteIssuerData,
- card = card
- )
- }
- Result.fromTangemSdkError(response.error)
- }
+ is CompletionResult.Failure -> Result.fromTangemSdkError(response.error)
}
}
@@ -103,13 +78,13 @@ class TwinCardsManager(
val issuer = getIssuers(reader).first { it.publicKey == publicKey }
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
- privateKey = issuer.privateKey.hexToBytes()
+ privateKey = issuer.privateKey.hexToBytes(),
)
}
private fun getAdapter(): JsonAdapter> {
return MoshiConverter.defaultMoshi().adapter(
- Types.newParameterizedType(List::class.java, Issuer::class.java)
+ Types.newParameterizedType(List::class.java, Issuer::class.java),
)
}
@@ -121,7 +96,6 @@ class TwinCardsManager(
}
private class Issuer(
- val id: String,
val privateKey: String,
val publicKey: String,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt
index e4ae0109bd..31d35c4a06 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt
@@ -13,7 +13,6 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.operations.issuerAndUserData.ReadIssuerDataResponse
import com.tangem.operations.issuerAndUserData.WriteIssuerDataCommand
import com.tangem.operations.sign.SignHashCommand
-import com.tangem.tap.domain.extensions.getSingleWallet
class WriteProtectedIssuerDataTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair,
@@ -25,7 +24,7 @@ class WriteProtectedIssuerDataTask(
) {
SignHashCommand(
twinPublicKey.calculateSha256(),
- session.environment.card!!.getSingleWallet()!!.publicKey
+ session.environment.card!!.wallets.first().publicKey,
)
.run(session) { signResult ->
when (signResult) {
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/WrongTwinCard.kt b/app/src/main/java/com/tangem/tap/domain/twins/WrongTwinCard.kt
new file mode 100644
index 0000000000..d05d28217e
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/twins/WrongTwinCard.kt
@@ -0,0 +1,14 @@
+package com.tangem.tap.domain.twins
+
+import com.tangem.common.core.TangemError
+import com.tangem.domain.common.TwinCardNumber
+import com.tangem.tap.tangemSdkManager
+import com.tangem.wallet.R
+
+data class WrongTwinCard(private val twinCardNumber: TwinCardNumber) : TangemError(code = 50005) {
+ override var customMessage: String = tangemSdkManager.getString(
+ R.string.twin_error_same_card,
+ twinCardNumber.number,
+ )
+ override val messageResId: Int? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt
new file mode 100644
index 0000000000..6291f7ae4d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt
@@ -0,0 +1,40 @@
+package com.tangem.tap.domain.userWalletList
+
+import com.tangem.common.core.TangemError
+import com.tangem.wallet.R
+
+sealed class UserWalletListError(code: Int) : TangemError(code) {
+ override val silent: Boolean
+ get() = (cause as? TangemError)?.silent == true
+
+ override val messageResId: Int? = null
+
+ object WalletAlreadySaved : UserWalletListError(code = 60001) {
+ override var customMessage: String = "This wallet has already been saved, you can add another one"
+ override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved
+ }
+
+ class SaveEncryptionKeysError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60001) {
+ override var customMessage: String = "Encryption keys could not be saved: ${cause.localizedMessage}"
+ }
+
+ class ReceiveEncryptionKeysError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60002) {
+ override var customMessage: String = "Encryption keys could not be received: ${cause.localizedMessage}"
+ }
+
+ class SaveSensitiveInformationError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60003) {
+ override var customMessage: String = "Sensitive information could not be saved: ${cause.localizedMessage}"
+ }
+
+ class ReceiveSensitiveInformationError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60004) {
+ override var customMessage: String = "Sensitive information could not be received: ${cause.localizedMessage}"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt
new file mode 100644
index 0000000000..9e7619e7c5
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt
@@ -0,0 +1,47 @@
+package com.tangem.tap.domain.userWalletList
+
+import com.tangem.common.CompletionResult
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.UserWallet
+import kotlinx.coroutines.flow.Flow
+
+interface UserWalletsListManager {
+ val userWallets: Flow>
+ val selectedUserWallet: Flow
+ val selectedUserWalletSync: UserWallet?
+ val isLocked: Flow
+ val isLockedSync: Boolean
+ val hasSavedUserWallets: Boolean
+
+ suspend fun unlockWithBiometry(): CompletionResult
+ fun lock()
+
+ suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult
+
+ /**
+ * Save provided user wallet and set it as selected
+ * @param userWallet [UserWallet] to save
+ * @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries to save an
+ * already saved card
+ * @return [CompletionResult] of operation
+ * */
+ suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult
+
+ /**
+ * Same as [save] but not change selected user wallet ID
+ * and not terminate with [UserWalletListError.WalletAlreadySaved] if [UserWallet] already saved
+ *
+ * Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId]
+ * @param userWalletId update [UserWallet] with that [UserWalletId]
+ * @param update lambda that receives stored [UserWallet] and returns updated [UserWallet]
+ * @return [CompletionResult] of operation with updated [UserWallet]
+ * */
+ suspend fun update(userWalletId: UserWalletId, update: (UserWallet) -> UserWallet): CompletionResult
+
+ suspend fun delete(userWalletIds: List): CompletionResult
+ suspend fun clear(): CompletionResult
+
+ suspend fun get(userWalletId: UserWalletId): CompletionResult
+
+ companion object
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
new file mode 100644
index 0000000000..3773da7127
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
@@ -0,0 +1,71 @@
+package com.tangem.tap.domain.userWalletList.di
+
+import android.content.Context
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import com.tangem.common.json.TangemSdkAdapter
+import com.tangem.common.services.secure.SecureStorage
+import com.tangem.tangem_sdk_new.storage.AndroidSecureStorage
+import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences
+import com.tangem.tap.domain.TangemSdkManager
+import com.tangem.tap.domain.userWalletList.UserWalletsListManager
+import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
+import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
+import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
+import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
+import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
+import com.tangem.tap.domain.userWalletList.utils.json.ByteArrayKeyAdapter
+import com.tangem.tap.domain.userWalletList.utils.json.CardBackupStatusAdapter
+import com.tangem.tap.domain.userWalletList.utils.json.ExtendedPublicKeysMapAdapter
+import com.tangem.tap.domain.userWalletList.utils.json.ScanResponseDerivedKeysMapAdapter
+import com.tangem.tap.domain.userWalletList.utils.json.WalletDerivedKeysMapAdapter
+
+private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
+
+fun UserWalletsListManager.Companion.provideBiometricImplementation(
+ context: Context,
+ tangemSdkManager: TangemSdkManager,
+): UserWalletsListManager {
+ val moshi = Moshi.Builder()
+ .add(WalletDerivedKeysMapAdapter())
+ .add(ScanResponseDerivedKeysMapAdapter())
+ .add(ByteArrayKeyAdapter())
+ .add(ExtendedPublicKeysMapAdapter())
+ .add(CardBackupStatusAdapter())
+ .add(TangemSdkAdapter.DateAdapter())
+ .add(TangemSdkAdapter.DerivationPathAdapter())
+ .add(TangemSdkAdapter.DerivationNodeAdapter())
+ .add(KotlinJsonAdapterFactory())
+ .build()
+
+ val secureStorage = AndroidSecureStorage(
+ preferences = SecureStorage.createEncryptedSharedPreferences(
+ context = context,
+ storageName = USER_WALLETS_STORAGE_NAME,
+ ),
+ )
+
+ val keysRepository = BiometricUserWalletsKeysRepository(
+ moshi = moshi,
+ secureStorage = secureStorage,
+ biometricManager = tangemSdkManager.biometricManager,
+ )
+ val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
+ moshi = moshi,
+ secureStorage = secureStorage,
+ )
+ val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
+ moshi = moshi,
+ secureStorage = secureStorage,
+ )
+ val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
+ secureStorage = secureStorage,
+ )
+
+ return BiometricUserWalletsListManager(
+ keysRepository = keysRepository,
+ publicInformationRepository = publicInformationRepository,
+ sensitiveInformationRepository = sensitiveInformationRepository,
+ selectedUserWalletRepository = selectedUserWalletRepository,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt
new file mode 100644
index 0000000000..db1db2ebbd
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt
@@ -0,0 +1,298 @@
+package com.tangem.tap.domain.userWalletList.implementation
+
+import com.tangem.common.*
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.UserWallet
+import com.tangem.tap.domain.userWalletList.UserWalletListError
+import com.tangem.tap.domain.userWalletList.UserWalletsListManager
+import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
+import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
+import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
+import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
+import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
+import com.tangem.tap.domain.userWalletList.utils.encryptionKey
+import com.tangem.tap.domain.userWalletList.utils.toUserWallets
+import com.tangem.tap.domain.userWalletList.utils.updateWith
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.*
+import timber.log.Timber
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class BiometricUserWalletsListManager(
+ private val keysRepository: UserWalletsKeysRepository,
+ private val publicInformationRepository: UserWalletsPublicInformationRepository,
+ private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
+ private val selectedUserWalletRepository: SelectedUserWalletRepository,
+) : UserWalletsListManager {
+ private val state = MutableStateFlow(State())
+
+ override val userWallets: Flow>
+ get() = state
+ .mapLatest { it.userWallets }
+ .distinctUntilChanged()
+
+ override val selectedUserWallet: Flow
+ get() = state
+ .mapLatest { state ->
+ findSelectedUserWallet(state.userWallets)
+ }
+ .filterNotNull()
+ .distinctUntilChanged()
+
+ override val selectedUserWalletSync: UserWallet?
+ get() = findSelectedUserWallet()
+
+ override val isLocked: Flow
+ get() = state
+ .mapLatest { it.isLocked }
+ .distinctUntilChanged()
+
+ override val isLockedSync: Boolean
+ get() = state.value.isLocked
+
+ override val hasSavedUserWallets: Boolean
+ get() = keysRepository.hasSavedEncryptionKeys()
+
+ override suspend fun unlockWithBiometry(): CompletionResult {
+ return unlockWithBiometryInternal()
+ .map { selectedUserWalletSync }
+ }
+
+ override fun lock() {
+ state.update { prevState ->
+ prevState.copy(
+ encryptionKeys = emptyList(),
+ isLocked = true,
+ )
+ }
+ }
+
+ override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult = catching {
+ if (state.value.selectedUserWalletId == userWalletId) {
+ return@catching findSelectedUserWallet()!!
+ }
+
+ selectedUserWalletRepository.set(userWalletId)
+
+ state.update { prevState ->
+ prevState.copy(
+ selectedUserWalletId = userWalletId,
+ )
+ }
+
+ findSelectedUserWallet()!!
+ }
+
+ override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult