Updated on 2026-08-14
This commit is contained in:
commit
9ffebc7634
566 changed files with 22478 additions and 5796 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -29,3 +29,4 @@ local.properties
|
|||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
/buildSrc/build/
|
||||
|
|
|
|||
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
|
|
@ -12,6 +12,7 @@
|
|||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<Properties>
|
||||
|
|
@ -199,4 +200,4 @@
|
|||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
|||
2
.idea/codeStyles/codeStyleConfig.xml
generated
2
.idea/codeStyles/codeStyleConfig.xml
generated
|
|
@ -2,4 +2,4 @@
|
|||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
|||
4
.idea/inspectionProfiles/Project_Default.xml
generated
4
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -61,8 +61,8 @@
|
|||
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantSetter" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantSuspendModifier" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitExpression" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitReturnType" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitExpression" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="RedundantUnitReturnType" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="RedundantVisibilityModifier" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantWith" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RemoveCurlyBracesFromTemplate" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
|
|
|
|||
187
app/build.gradle
187
app/build.gradle
|
|
@ -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'
|
||||
|
||||
}
|
||||
208
app/build.gradle.kts
Normal file
208
app/build.gradle.kts
Normal file
|
|
@ -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<com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension> {
|
||||
// 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)
|
||||
}
|
||||
Binary file not shown.
BIN
app/libs/walletconnect-1.5.6.aar
Normal file
BIN
app/libs/walletconnect-1.5.6.aar
Normal file
Binary file not shown.
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.wallet.api.enabled"
|
||||
|
|
@ -102,6 +101,17 @@
|
|||
<data android:scheme="wc" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="wc" />
|
||||
<data android:scheme="tangem" />
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
@ -142,5 +152,4 @@
|
|||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
8
app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
Normal file
8
app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
|
||||
interface ActivityResultCaller {
|
||||
val activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
}
|
||||
|
|
@ -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<Intent>? = null
|
||||
private set
|
||||
|
||||
private val activities = WeakHashMap<KClass<out Activity>, 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) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
Normal file
117
app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OnActivityResultCallback>()
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppState>
|
||||
|
||||
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<Any> = 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) {
|
||||
|
|
|
|||
|
|
@ -76,13 +76,13 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
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<GlobalState> {
|
|||
)
|
||||
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<GlobalState> {
|
|||
)
|
||||
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<GlobalState> {
|
|||
primaryButtonAction = state.dialog.onOk,
|
||||
)
|
||||
is WalletDialog.RussianCardholdersWarningDialog ->
|
||||
RussianCardholdersWarningBottomSheetDialog(context, state.dialog.topUpUrl)
|
||||
RussianCardholdersWarningBottomSheetDialog(context, state.dialog.data)
|
||||
else -> null
|
||||
}
|
||||
dialog?.show()
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, AnalyticsHandler>()
|
||||
private val paramsInterceptors = mutableMapOf<String, ParamsInterceptor>()
|
||||
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
|
||||
|
||||
private val analyticsHandlers: List<AnalyticsHandler>
|
||||
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<String, String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AnalyticsHandlerBuilder>()
|
||||
private val filters = mutableListOf<AnalyticsEventFilter>()
|
||||
private val interceptors = mutableListOf<ParamsInterceptor>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
|
||||
|
|
@ -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<AnalyticsEventHandler>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>) {}
|
||||
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {}
|
||||
override fun handleCardSdkErrorEvent(
|
||||
error: TangemSdkError,
|
||||
action: AnalyticsAnOld.ActionToLog,
|
||||
params: Map<AnalyticsParamAnOld, String>,
|
||||
card: Card?,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun handleBlockchainSdkErrorEvent(
|
||||
error: BlockchainError,
|
||||
action: AnalyticsAnOld.ActionToLog,
|
||||
params: Map<AnalyticsParamAnOld, String>,
|
||||
card: Card?,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun handleShopifyOrderEvent(order: Storefront.Order) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalAnalyticsHandler(
|
||||
private val analyticsHandlers: List<AnalyticsEventHandler>,
|
||||
) : GlobalAnalyticsEventHandler {
|
||||
|
||||
private val attachToAllEventsParams: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
override fun attachToAllEvents(key: String, value: String) {
|
||||
attachToAllEventsParams[key] = value
|
||||
}
|
||||
|
||||
override fun handleEvent(event: String, params: Map<String, String>) {
|
||||
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<String, String>,
|
||||
card: Card?,
|
||||
blockchain: String?,
|
||||
) {
|
||||
analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) }
|
||||
}
|
||||
|
||||
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {
|
||||
analyticsHandlers.filterIsInstance<ErrorEventLogger>().forEach {
|
||||
it.logErrorEvent(error, params)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleCardSdkErrorEvent(
|
||||
error: TangemSdkError,
|
||||
action: AnalyticsAnOld.ActionToLog,
|
||||
params: Map<AnalyticsParamAnOld, String>,
|
||||
card: Card?,
|
||||
) {
|
||||
analyticsHandlers.filterIsInstance<CardSdkErrorEventHandler>().forEach {
|
||||
it.handleCardSdkErrorEvent(error, action, params, card)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleBlockchainSdkErrorEvent(
|
||||
error: BlockchainError,
|
||||
action: AnalyticsAnOld.ActionToLog,
|
||||
params: Map<AnalyticsParamAnOld, String>,
|
||||
card: Card?,
|
||||
) {
|
||||
analyticsHandlers.filterIsInstance<BlockchainSdkErrorEventHandler>().forEach {
|
||||
it.handleBlockchainSdkErrorEvent(error, action, params, card)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleShopifyOrderEvent(order: Storefront.Order) {
|
||||
analyticsHandlers.filterIsInstance<ShopifyOrderEventHandler>().forEach {
|
||||
it.handleShopifyOrderEvent(order)
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepareParams(card: Card?, blockchain: String?, params: Map<String, String>): Map<String, String> {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<String, String> = 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<String, String> = emptyMap())
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
send(prepareEventString(event), event.params)
|
||||
}
|
||||
|
||||
@Deprecated("Migrate to AnalyticsEvent")
|
||||
fun handleAnalyticsEvent(
|
||||
event: AnalyticsEventAnOld,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
card: Card? = null,
|
||||
blockchain: String? = null,
|
||||
) {
|
||||
handleEvent(event.event, prepareParams(card, blockchain, params))
|
||||
}
|
||||
|
||||
fun prepareParams(
|
||||
card: Card? = null,
|
||||
blockchain: String? = null,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
): Map<String, String> = 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<String, String> = 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<AnalyticsParamAnOld, String> = 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<AnalyticsParamAnOld, String> = mapOf(),
|
||||
card: Card? = null,
|
||||
)
|
||||
}
|
||||
|
||||
interface ShopifyOrderEventHandler {
|
||||
fun handleShopifyOrderEvent(order: Storefront.Order)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>)
|
||||
}
|
||||
|
||||
interface ParamsInterceptorHolder {
|
||||
fun addParamsInterceptor(interceptor: ParamsInterceptor)
|
||||
fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor?
|
||||
}
|
||||
|
|
@ -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<Throwable, Map<String, String>> {
|
||||
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<String, String> {
|
||||
return when (value) {
|
||||
is BlockchainSdkError -> BlockchainSdkErrorConverter(CardSdkErrorConverter()).convert(value)
|
||||
is TangemSdkError -> CardSdkErrorConverter().convert(value)
|
||||
else -> ThrowableErrorConverter().convert(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ThrowableErrorConverter : Converter<Throwable, Map<String, String>> {
|
||||
override fun convert(value: Throwable): Map<String, String> {
|
||||
val unknown = "unknown"
|
||||
return mapOf(
|
||||
AnalyticsParam.ErrorKey to AnalyticsParam.Error.App.value,
|
||||
AnalyticsParam.ErrorDescription to (value.message ?: value.cause?.message ?: unknown),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CardSdkErrorConverter : Converter<TangemSdkError, Map<String, String>> {
|
||||
|
||||
override fun convert(value: TangemSdkError): Map<String, String> {
|
||||
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<BlockchainSdkError, Map<String, String>> {
|
||||
|
||||
override fun convert(value: BlockchainSdkError): Map<String, String> {
|
||||
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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletState, Basic.SignedIn?> {
|
||||
|
||||
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<WalletState, Basic.ToppedUp?> {
|
||||
|
||||
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<WalletData>): 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
|
||||
}
|
||||
|
|
@ -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<ScanResponse, AnalyticsParam.CardCurrency?> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>,
|
||||
)
|
||||
|
||||
internal class CardSdkErrorConverter : Converter<CardSdkErrorConverter.Model, ErrorEvent?> {
|
||||
|
||||
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<AnalyticsParamAnOld, String>,
|
||||
val cardParams: Map<String, String>,
|
||||
)
|
||||
}
|
||||
|
||||
internal class BlockchainSdkErrorConverter(
|
||||
private val cardSdkErrorConverter: CardSdkErrorConverter,
|
||||
) : Converter<BlockchainSdkErrorConverter.Model, ErrorEvent?> {
|
||||
|
||||
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<AnalyticsParamAnOld, String>,
|
||||
val cardParams: Map<String, String>,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Map<AnalyticsParamAnOld, String>.convertToParamValue(): Map<String, String> {
|
||||
return mutableMapOf<String, String>().also { newMap ->
|
||||
this.forEach {
|
||||
newMap[it.key.param] = it.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Pair<Storefront.Order, ProductType>, Shop.Purchased> {
|
||||
|
||||
override fun convert(value: Pair<Storefront.Order, ProductType>): 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,5 +6,7 @@ package com.tangem.tap.common.analytics.events
|
|||
sealed class AnalyticsEvent(
|
||||
val category: String,
|
||||
val event: String,
|
||||
val params: Map<String, String> = mapOf(),
|
||||
var params: Map<String, String> = mapOf(),
|
||||
val error: Throwable? = null,
|
||||
var filterData: Any? = null,
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,11 @@ package com.tangem.tap.common.analytics.events
|
|||
sealed class Basic(
|
||||
event: String,
|
||||
params: Map<String, String> = 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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<String, String> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
sealed class MyWallets(
|
||||
event: String,
|
||||
params: Map<String, String> = 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String, String> = 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<String, String> = 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<String, String> = 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<String, String> = 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<String, String> = 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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ package com.tangem.tap.common.analytics.events
|
|||
sealed class WalletConnect(
|
||||
event: String,
|
||||
params: Map<String, String> = 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)
|
||||
}
|
||||
|
|
@ -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<String>()
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>) {
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>) {
|
||||
logger.logEvent(event, params)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String>) {
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
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<Storefront.Order, Map<String, String>> {
|
||||
|
||||
override fun convert(value: Storefront.Order): Map<String, String> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>) {
|
||||
logger.logEvent(event, params)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String>) {
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
client.logEvent(event, params)
|
||||
}
|
||||
|
||||
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {
|
||||
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<String, String>) {
|
||||
client.logErrorEvent(error, params)
|
||||
}
|
||||
|
||||
override fun handleCardSdkErrorEvent(
|
||||
error: TangemSdkError,
|
||||
action: AnalyticsAnOld.ActionToLog,
|
||||
params: Map<AnalyticsParamAnOld, String>,
|
||||
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<AnalyticsParamAnOld, String>,
|
||||
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<Storefront.Order, Map<String, String>> {
|
||||
override fun convert(value: Storefront.Order): Map<String, String> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>) {
|
||||
logger.logEvent(event, params)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String>) {
|
||||
params[AnalyticsParam.BatchId] = batchId
|
||||
}
|
||||
}
|
||||
|
|
@ -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") {}
|
||||
|
|
|
|||
|
|
@ -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 <T> SimpleDialog(
|
|||
modifier = Modifier.padding(22.dp)
|
||||
) {
|
||||
DialogTitle(title = title)
|
||||
LazyColumn() {
|
||||
LazyColumn {
|
||||
items(items) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -154,4 +155,4 @@ fun ErrorDialog(
|
|||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -62,4 +62,9 @@ fun String.toQrCode(): Bitmap {
|
|||
return bmp
|
||||
}
|
||||
|
||||
fun String.urlEncode(): String = Uri.encode(this)
|
||||
fun String.urlEncode(): String = Uri.encode(this)
|
||||
|
||||
fun String.removePrefixOrNull(prefix: String): String? = when {
|
||||
startsWith(prefix) -> substring(prefix.length)
|
||||
else -> null
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,11 +55,10 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = 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,
|
||||
|
|
|
|||
|
|
@ -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<EmailWalletInfo>()
|
||||
|
|
@ -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<WalletManager>) {
|
||||
|
|
@ -89,7 +92,7 @@ class AdditionalFeedbackInfo {
|
|||
)
|
||||
}
|
||||
|
||||
private fun formatSignedHashes(wallets: List<CardWallet>): String {
|
||||
private fun formatSignedHashes(wallets: List<CardDTO.Wallet>): String {
|
||||
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
16
app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt
Normal file
16
app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt
Normal file
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<AppState> = { _, _ ->
|
||||
{ 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Middleware<AppState>> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.tap.lockUserWalletsTimer
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class LockUserWalletsTimerMiddleware {
|
||||
val middleware: Middleware<AppState> = { _, _ ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
lockUserWalletsTimer?.restart()
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<AppCompatActivity>) : NavigationAction()
|
||||
|
|
|
|||
|
|
@ -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<AppState> = { dispatch, state ->
|
||||
val navigationMiddleware: Middleware<AppState> = { _, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
if (action is NavigationAction) {
|
||||
|
|
@ -16,16 +22,24 @@ val navigationMiddleware: Middleware<AppState> = { 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<AppState> = { 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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import org.rekotlin.StateType
|
|||
import java.lang.ref.WeakReference
|
||||
|
||||
data class NavigationState(
|
||||
val backStack: List<AppScreen> = listOf(AppScreen.Home),
|
||||
val backStack: List<AppScreen> = emptyList(),
|
||||
val activity: WeakReference<AppCompatActivity>? = 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),
|
||||
}
|
||||
|
|
@ -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<ProductType, Storefront.Checkout>()
|
||||
private val variants = mutableMapOf<ProductType, Storefront.ProductVariant>()
|
||||
|
||||
|
|
@ -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<Unit> {
|
||||
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<Storefront.Checkout> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Blockchain>? = null,
|
||||
messageRes: Int? = null,
|
||||
): CompletionResult<ScanResponse> {
|
||||
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<ScanResponse>,
|
||||
) {
|
||||
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<Card> {
|
||||
suspend fun createWallet(cardId: String?): CompletionResult<CardDTO> {
|
||||
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<Card> {
|
||||
suspend fun resetToFactorySettings(cardId: String): CompletionResult<CardDTO> {
|
||||
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<String>): CompletionResult<Unit> {
|
||||
return userCodeRepository.save(
|
||||
cardsIds = cardsIds,
|
||||
userCode = UserCode(
|
||||
type = UserCodeType.AccessCode,
|
||||
stringValue = accessCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun deleteSavedUserCodes(cardsIds: Set<String>): CompletionResult<Unit> {
|
||||
return userCodeRepository.delete(cardsIds.toSet())
|
||||
}
|
||||
|
||||
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
|
||||
return userCodeRepository.clear()
|
||||
}
|
||||
|
||||
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
|
||||
|
|
@ -154,11 +176,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun scanCard(): CompletionResult<Card> {
|
||||
suspend fun scanCard(
|
||||
cardId: String? = null,
|
||||
allowRequestAccessCodeFromRepository: Boolean = false,
|
||||
): CompletionResult<CardDTO> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
ScanTask(),
|
||||
runnable = ScanTask(allowRequestAccessCodeFromRepository),
|
||||
cardId = cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
|
||||
)
|
||||
.map { CardDTO(it) }
|
||||
}
|
||||
|
||||
suspend fun <T : CommandResponse> runTaskAsync(
|
||||
|
|
@ -176,7 +203,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
}
|
||||
|
||||
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
|
||||
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
|
||||
runnable: CardSessionRunnable<T>,
|
||||
cardId: String? = null,
|
||||
initialMessage: Message? = null,
|
||||
): CompletionResult<T> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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<BlockchainNetwork>, 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<CardVerifyAndGetInfo.Response.Item>? = null): String {
|
||||
suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = 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<CardVerifyAndGetInfo.R
|
|||
}
|
||||
}
|
||||
|
||||
val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)
|
||||
return when (cardInfoResult) {
|
||||
return when (val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
|
||||
is Result.Success -> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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>): CardWallet? {
|
||||
private fun selectWallet(wallets: List<CardDTO.Wallet>): CardDTO.Wallet? {
|
||||
return when (wallets.size) {
|
||||
0 -> null
|
||||
1 -> wallets[0]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
35
app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
Normal file
35
app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
Normal file
|
|
@ -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<String>,
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<AddressData>,
|
||||
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<PendingTransaction> = 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<PendingTransaction>,
|
||||
) : Status()
|
||||
|
||||
data class SameCurrencyTransactionInProgress(
|
||||
override val amount: BigDecimal,
|
||||
override val pendingTransactions: List<PendingTransaction>,
|
||||
) : 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletDataModel>,
|
||||
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
|
||||
}
|
||||
|
|
@ -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<String> = 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<String>?) = 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CardDTO.Wallet>): ByteArray? {
|
||||
return wallets.firstOrNull()
|
||||
?.publicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletDataModel> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<Blockchain>? = 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
|
||||
val primaryCard: PrimaryCard? = null
|
||||
) : CommandResponse
|
||||
val primaryCard: PrimaryCard? = null,
|
||||
) : CommandResponse {
|
||||
constructor(
|
||||
card: Card,
|
||||
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = 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<CreateProductWalletTaskResponse> {
|
||||
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false
|
||||
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> 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<CreateWalletResponse> {
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -94,52 +121,65 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
|
|||
} else {
|
||||
intersectCurves[0]
|
||||
}
|
||||
CreateWalletTask(curve).run(session, callback)
|
||||
CreateWalletTask(curve).run(session) { result ->
|
||||
callback(result.map { CreateWalletResponse(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWalletTaskResponse> {
|
||||
|
||||
private lateinit var card: Card
|
||||
private var primaryCard: PrimaryCard? = null
|
||||
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> 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<CreateWalletResponse>,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> 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<CreateProductWa
|
|||
}
|
||||
|
||||
private fun linkPrimaryCard(
|
||||
card: CardDTO,
|
||||
createWalletResponse: List<CreateWalletResponse>,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
|
|
@ -161,8 +202,9 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
primaryCard = result.data
|
||||
when {
|
||||
card.settings.isHDWalletAllowed -> {
|
||||
deriveKeys(createWalletResponse, session, callback)
|
||||
deriveKeys(card, createWalletResponse, session, callback)
|
||||
}
|
||||
|
||||
else -> {
|
||||
callback(
|
||||
CompletionResult.Success(
|
||||
|
|
@ -174,6 +216,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
|
|
@ -182,13 +225,14 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
}
|
||||
|
||||
private fun deriveKeys(
|
||||
card: CardDTO,
|
||||
createWalletResponse: List<CreateWalletResponse>,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
) {
|
||||
val map = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
|
||||
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<CreateProductWa
|
|||
CreateProductWalletTaskResponse(
|
||||
card = session.environment.card!!,
|
||||
derivedKeys = result.data.entries,
|
||||
primaryCard = primaryCard
|
||||
)
|
||||
)
|
||||
primaryCard = primaryCard,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockchains(cardId: String): List<Blockchain> {
|
||||
private fun getBlockchains(
|
||||
cardId: String,
|
||||
card: CardDTO,
|
||||
): List<Blockchain> {
|
||||
return when {
|
||||
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains
|
||||
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
|
||||
|
|
|
|||
|
|
@ -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<T> {
|
||||
fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<T>) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Blockchain>? = null,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean
|
||||
get() = !additionalBlockchainsToDerive.isNullOrEmpty()
|
||||
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> 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<ScanResponse> {
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -118,7 +122,7 @@ private class ScanWalletProcessor(
|
|||
|
||||
var primaryCard: PrimaryCard? = null
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -126,7 +130,7 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
|
||||
private fun createMissingWalletsIfNeeded(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -160,13 +164,15 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
|
||||
private fun startLinkingForBackupIfNeeded(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> 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<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -227,25 +233,41 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
|
||||
private suspend fun getBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> {
|
||||
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<ByteArrayKey, List<DerivationPath>> {
|
||||
private suspend fun collectDerivations(card: CardDTO): Map<ByteArrayKey, List<DerivationPath>> {
|
||||
val blockchains = getBlockchainsToDerive(card)
|
||||
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
|
||||
|
||||
|
|
@ -287,14 +309,14 @@ private class ScanWalletProcessor(
|
|||
|
||||
private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> 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<ScanResponse> {
|
|||
}
|
||||
}
|
||||
|
||||
fun Card.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
|
||||
fun CardDTO.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
|
||||
val curvesPresent = wallets.map { it.curve }.toSet()
|
||||
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
|
||||
return curvesForNonCreatedWallets.toList()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Blockchain> {
|
||||
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
|
||||
|
|
@ -26,6 +27,4 @@ object CurrenciesRepository {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue