Updated on 2026-08-14

This commit is contained in:
Tangem 2022-08-23 13:51:54 +03:00
commit 6bd93514b7
285 changed files with 8531 additions and 4253 deletions

14
.editorconfig Normal file
View file

@ -0,0 +1,14 @@
[*.{kt,kts}]
ktlint_ignore_back_ticked_identifier = true
indent_size = 4
indent_style = space
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**
max_line_length = 120

4
.gitignore vendored
View file

@ -14,6 +14,8 @@ local.properties
# User-specific configurations
.idea
!.idea/codeStyles
!.idea/inspectionProfiles
# LayoutInspector files
/captures
@ -26,4 +28,4 @@ local.properties
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
**/fastlane/test_output

204
.idea/codeStyles/Project.xml generated Normal file
View file

@ -0,0 +1,204 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<option name="SOFT_MARGINS" value="120" />
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<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="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<Properties>
<option name="SPACES_AROUND_KEY_VALUE_DELIMITER" value="true" />
<option name="KEEP_BLANK_LINES" value="true" />
</Properties>
<codeStyleSettings language="CMake">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="EditorConfig">
<option name="ALIGN_GROUP_FIELD_DECLARATIONS" value="true" />
</codeStyleSettings>
<codeStyleSettings language="Groovy">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="LINE_COMMENT_ADD_SPACE_ON_REFORMAT" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Markdown">
<option name="RIGHT_MARGIN" value="120" />
<option name="WRAP_ON_TYPING" value="1" />
</codeStyleSettings>
<codeStyleSettings language="Properties">
<option name="ALIGN_GROUP_FIELD_DECLARATIONS" value="true" />
</codeStyleSettings>
<codeStyleSettings language="XML">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="RIGHT_MARGIN" value="120" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="LINE_COMMENT_ADD_SPACE_ON_REFORMAT" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="0" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="EXTENDS_LIST_WRAP" value="5" />
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
<option name="ASSIGNMENT_WRAP" value="5" />
<option name="PARAMETER_ANNOTATION_WRAP" value="5" />
<option name="VARIABLE_ANNOTATION_WRAP" value="5" />
<option name="ENUM_CONSTANTS_WRAP" value="5" />
<option name="WRAP_ON_TYPING" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="protobuf">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="prototext">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View file

@ -0,0 +1,92 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CanBeParameter" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="CanBePrimaryConstructorProperty" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ComplexRedundantLet" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ConstantConditionIf" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ExplicitThis" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="KotlinDoubleNegation" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="KotlinRedundantOverride" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MemberVisibilityCanBePrivate" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NullChecksToSafeCall" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="RedundantCompanionReference" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantElvisReturnNull" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantEmptyInitializerBlock" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantEnumConstructorInvocation" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantExplicitType" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantGetter" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantIf" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantInnerClassModifier" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantLambdaArrow" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantLambdaOrAnonymousFunction" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantModalityModifier" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantNullableReturnType" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantRequireNotNullCall" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantReturnLabel" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantSamConstructor" enabled="true" level="ERROR" enabled_by_default="true" />
<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="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" />
<inspection_tool class="RemoveEmptyClassBody" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveEmptyParenthesesFromLambdaCall" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveEmptyPrimaryConstructor" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveEmptySecondaryConstructorBody" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveExplicitSuperQualifier" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveExplicitTypeArguments" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveForLoopIndices" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveRedundantBackticks" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveRedundantCallsOfConversionMethods" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveRedundantQualifierName" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveRedundantSpreadOperator" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveSetterParameterType" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveSingleExpressionStringTemplate" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RemoveToStringInStringTemplate" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ReplaceWithIgnoreCaseEquals" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SimpleRedundantLet" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SimplifyWhenWithBooleanConstantCondition" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="UnnecessaryOptInAnnotation" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="UnnecessaryVariable" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="UnusedReceiverParameter" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="UnusedSymbol" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="WhenWithOnlyElse" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>

View file

@ -6,13 +6,13 @@ apply plugin: 'com.google.firebase.crashlytics'
apply from: '../dependencies.gradle'
android {
compileSdkVersion 31
compileSdkVersion 32
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 31
targetSdkVersion 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
@ -83,20 +83,18 @@ dependencies {
implementation implementation(project(path: ':network'))
implementation implementation(project(path: ':common'))
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.google.android.material:material:1.5.0'
implementation "androidx.core:core-ktx:1.7.0"
implementation 'com.google.android.play:core:1.10.3'
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:AND-1939_tron_fix_release-97'
// implementation 'com.tangem:blockchain:0.0.1'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154'
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.3.0"
@ -145,8 +143,11 @@ dependencies {
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:logging-interceptor")
//
// //Crypto
// 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
@ -162,14 +163,14 @@ dependencies {
implementation 'com.google.android.gms:play-services-wallet:19.1.0'
//Compose
implementation 'androidx.activity:activity-compose:1.4.0'
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.1.1'
implementation 'androidx.compose.foundation:foundation-layout: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"

Binary file not shown.

Binary file not shown.

View file

@ -28,14 +28,15 @@
</queries>
<application
android:name="com.tangem.tap.TapApplication"
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:label="@string/app_name"
android:name="com.tangem.tap.TapApplication"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:allowBackup="true"
android:fullBackupContent="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:fullBackupContent">
@ -46,7 +47,7 @@
<activity
android:exported="true"
android:name="com.tangem.tap.MainActivity"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@ -127,6 +128,21 @@
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
<activity android:name="zendesk.messaging.MessagingActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.guide.HelpCenterActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.guide.ViewArticleActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.request.RequestActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.requestlist.RequestListActivity"
android:theme="@style/ZendeskTheme" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
@ -139,4 +155,4 @@
</application>
</manifest>
</manifest>

@ -1 +1 @@
Subproject commit bf673f7f88ed561cd8827447c4886f80f30b7e23
Subproject commit f33f020823031f1c532cf5a9d5cf3fbecadcf050

View file

@ -0,0 +1,46 @@
package com.tangem.tap
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import java.util.WeakHashMap
import kotlin.reflect.KClass
class ForegroundActivityObserver {
private val activities = WeakHashMap<KClass<out Activity>, Activity>()
val foregroundActivity: Activity?
get() = activities.entries
.filterNot { it.value.isDestroyed }
.firstOrNull()
?.value
val callbacks get() = Callbacks()
inner class Callbacks : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
activities[activity::class] = activity
}
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity::class)
}
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) {
}
}
}
fun ForegroundActivityObserver.withForegroundActivity(
block: (Activity) -> Unit
) {
foregroundActivity?.let { block(it) }
}

View file

@ -5,6 +5,8 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk
@ -57,7 +59,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
systemActions()
store.state.globalState.feedbackManager?.updateActivity(this)
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
@ -84,8 +85,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
}
private fun systemActions() {
// makes the status bar text dark
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
WindowCompat.setDecorFitsSystemWindows(window, false)
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)
windowInsetsController.isAppearanceLightStatusBars = true
windowInsetsController.isAppearanceLightNavigationBars = true
supportFragmentManager.registerFragmentLifecycleCallbacks(
NavBarInsetsFragmentLifecycleCallback(),
true
)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap
import android.os.Bundle
import android.view.View
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
class NavBarInsetsFragmentLifecycleCallback : FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(
fm: FragmentManager,
f: Fragment,
v: View,
savedInstanceState: Bundle?,
) {
if (v is ComposeView) return
ViewCompat.setOnApplyWindowInsetsListener(v) { view, windowInsets ->
val statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
val navigationBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
if (view.fitsSystemWindows) {
view.updatePadding(
top = statusBarInsets.top,
bottom = navigationBarInsets.bottom,
)
} else {
view.updatePadding(
bottom = navigationBarInsets.bottom,
)
}
windowInsets
}
}
}

View file

@ -8,11 +8,15 @@ import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.domain.DomainLayer
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
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.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
@ -24,9 +28,6 @@ import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.features.feedback.AdditionalEmailInfo
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.feedback.TangemLogCollector
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.wallet.BuildConfig
@ -40,6 +41,8 @@ val store = Store(
)
val logConfig = LogConfig()
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
@ -68,6 +71,8 @@ class TapApplication : Application(), ImageLoaderFactory {
)
walletConnectRepository = WalletConnectRepository(this)
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
initFeedbackManager()
loadConfigs()
@ -85,22 +90,51 @@ class TapApplication : Application(), ImageLoaderFactory {
val localLoader = FeaturesLocalLoader(this, moshi)
val remoteLoader = FeaturesRemoteLoader(moshi)
val configManager = ConfigManager(localLoader, remoteLoader)
configManager.load {
configManager.load { config ->
store.dispatch(GlobalAction.SetConfigManager(configManager))
shopService = TangemShopService(this, configManager.config.shopify!!)
shopService = TangemShopService(
application = this,
shopifyShop = config.shopify!!
)
store.state.globalState.feedbackManager?.initChat(
context = this,
zendeskConfig = config.zendesk!!
)
}
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
}
private fun initFeedbackManager() {
val infoHolder = AdditionalEmailInfo()
val infoHolder = AdditionalFeedbackInfo()
infoHolder.setAppVersion(this)
val logWriter = TangemLogCollector()
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
)
val logWriter = TangemLogCollector(
levels = logLevels,
messageFormatter = LogFormat.StairsFormatter(),
)
Log.addLogger(logWriter)
store.dispatch(GlobalAction.SetFeedbackManager(FeedbackManager(infoHolder, logWriter)))
store.dispatch(
GlobalAction.SetFeedbackManager(
FeedbackManager(
infoHolder = infoHolder,
logCollector = logWriter,
preferencesStorage = preferencesStorage,
),
),
)
}
private fun initAppsFlyer() {
@ -115,4 +149,5 @@ class TapApplication : Application(), ImageLoaderFactory {
data class LogConfig(
val coil: Boolean = BuildConfig.DEBUG,
val storeAction: Boolean = BuildConfig.DEBUG,
val zendesk: Boolean = BuildConfig.DEBUG,
)

View file

@ -4,12 +4,14 @@ import android.app.Dialog
import android.content.Context
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.BnbTransactionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ChooseNetworkDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.SimpleAlertDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
@ -19,7 +21,7 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBack
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ChooseTradeActionBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.RussianCardholdersWarningBottomSheetDialog
@ -73,22 +75,33 @@ class DialogManager : StoreSubscriber<GlobalState> {
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
context = context
)
is WalletConnectDialog.AddNetwork ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
message = context.getString(
R.string.wallet_connect_network_not_found_format,
state.dialog.network
),
context = context
)
is WalletConnectDialog.OpeningSessionRejected -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_same_wcuri,
context = context
context = context,
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_error_timeout,
context = context
context = context,
)
}
is WalletConnectDialog.ApproveWcSession ->
ApproveWcSessionDialog.create(state.dialog.session, context)
ApproveWcSessionDialog.create(state.dialog.session, state.dialog.networks, context)
is WalletConnectDialog.ChooseNetwork ->
ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context)
is WalletConnectDialog.ClipboardOrScanQr ->
ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
is WalletConnectDialog.RequestTransaction ->
@ -102,6 +115,12 @@ class DialogManager : StoreSubscriber<GlobalState> {
sessionId = state.dialog.sessionId,
cardId = state.dialog.cardId,
dAppName = state.dialog.dAppName,
context = context,
)
is WalletConnectDialog.UnsupportedNetwork ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
context = context
)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
@ -116,6 +135,20 @@ class DialogManager : StoreSubscriber<GlobalState> {
AmountToSendBottomSheetDialog(context, state.dialog)
is WalletDialog.SignedHashesMultiWalletDialog ->
SignedHashesWarningDialog.create(context)
is WalletDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
message = context.getString(
state.dialog.messageRes, state.dialog.currencySymbol, state.dialog.currencyTitle
),
context = context,
)
is WalletDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencyTitle),
messageRes = state.dialog.messageRes,
context = context,
primaryButtonRes = state.dialog.primaryButtonRes,
primaryButtonAction = state.dialog.onOk
)
is WalletDialog.RussianCardholdersWarningDialog ->
RussianCardholdersWarningBottomSheetDialog(context)
else -> null

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.common.compose
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -12,7 +11,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun TextAutoSize(
modifier: Modifier = Modifier,

View file

@ -38,6 +38,11 @@ fun SpacerH24(modifier: Modifier = Modifier) {
SpacerH(24.dp, modifier)
}
@Composable
fun SpacerH32(modifier: Modifier = Modifier) {
SpacerH(32.dp, modifier)
}
// ***************************** Vertical
@Composable
fun SpacerW(width: Dp, modifier: Modifier = Modifier) {
@ -64,6 +69,11 @@ fun SpacerW24(modifier: Modifier = Modifier) {
SpacerW(24.dp, modifier)
}
@Composable
fun SpacerW32(modifier: Modifier = Modifier) {
SpacerW(32.dp, modifier)
}
// ***************************** Size
@Composable
fun SpacerS(size: Dp, modifier: Modifier = Modifier) {
@ -88,4 +98,9 @@ fun SpacerS16(modifier: Modifier = Modifier) {
@Composable
fun SpacerS24(modifier: Modifier = Modifier) {
SpacerS(24.dp, modifier)
}
@Composable
fun SpacerS32(modifier: Modifier = Modifier) {
SpacerS(32.dp, modifier)
}

View file

@ -0,0 +1,71 @@
package com.tangem.tap.common.compose
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
object TangemTypography {
val body1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 16.0.sp,
letterSpacing = 0.5.sp,
lineHeight = 24.0.sp,
)
val body2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 14.0.sp,
letterSpacing = 0.25.sp,
lineHeight = 20.0.sp,
)
val button = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 14.0.sp,
letterSpacing = 0.10000000149011612.sp,
lineHeight = 20.0.sp,
)
val caption = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 12.0.sp,
letterSpacing = 0.4000000059604645.sp,
lineHeight = 16.0.sp,
)
val headline1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto)),
fontSize = 34.0.sp,
letterSpacing = 0.0.sp,
lineHeight = 44.0.sp,
)
val headline2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 24.0.sp,
letterSpacing = 0.18000000715255737.sp,
lineHeight = 32.0.sp,
)
val headline3 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 20.0.sp,
letterSpacing = 0.15000000596046448.sp,
lineHeight = 24.0.sp,
)
val overline = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 10.0.sp,
letterSpacing = 1.5.sp,
lineHeight = 16.0.sp,
)
val subtitle1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 16.0.sp,
letterSpacing = 0.15000000596046448.sp,
lineHeight = 24.0.sp,
)
val subtitle2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 14.0.sp,
letterSpacing = 0.10000000149011612.sp,
lineHeight = 24.0.sp,
)
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
/**
[REDACTED_AUTHOR]
*/
typealias AnimatedValue = Pair<Float, Float>
@Composable
fun AnimatedValue.toAnimatable(
isPaused: Boolean,
duration: Int,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
return animatable(
values = this,
isPaused = isPaused,
duration = duration,
easing = easing,
)
}
@Composable
fun animatable(
values: AnimatedValue,
duration: Int,
isPaused: Boolean = false,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
val animatable = remember { Animatable(values.first) }
LaunchedEffect(isPaused) {
if (isPaused) {
animatable.stop()
} else {
animatable.animateTo(
targetValue = values.second,
animationSpec = tween(
durationMillis = duration,
easing = easing
)
)
}
}
return animatable
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Dp.toPx(): Float {
val currentDp = this
return with(LocalDensity.current) { currentDp.toPx() }
}
fun DpSize.halfWidth(): Dp = this.width / 2
fun DpSize.halfHeight(): Dp = this.height / 2

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.compose.extensions
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
/**
[REDACTED_AUTHOR]
*/
@Composable
fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap {
val drawable = AppCompatResources.getDrawable(LocalContext.current, drawableId)
?: throw NullPointerException()
return drawable.toBitmap().asImageBitmap()
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.MutableState
/**
[REDACTED_AUTHOR]
*/
fun <T> MutableState<List<T>>.addAndNotify(value: T) {
this.value = this.value.toMutableList().apply { add(value) }
}
fun <T> MutableState<List<T>>.removeAndNotify(value: T) {
this.value = this.value.toMutableList().apply { remove(value) }
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.pxToDp
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.width.pxToDp().dp,
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.dpToPx(): Float = LocalContext.current.dpToPx(this)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)

View file

@ -0,0 +1,51 @@
package com.tangem.tap.common.extensions
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.common.BlockchainError
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.features.demo.DemoTransactionSender
/**
[REDACTED_AUTHOR]
*/
fun AnalyticsHandler.logSendTransactionError(
error: BlockchainError,
action: Analytics.ActionToLog,
parameters: Map<AnalyticsParam, String>? = mapOf(),
card: Card? = null,
) {
when (val blockchainSdkError = (error as BlockchainSdkError)) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = (blockchainSdkError.tangemError as? TangemSdkError) ?: return
logCardSdkError(
error = tangemSdkError,
actionToLog = action,
parameters = parameters,
card = card,
)
}
else -> {
when {
blockchainSdkError.customMessage.contains(DemoTransactionSender.ID) -> return
else -> {
val params = parameters?.toMutableMap() ?: mutableMapOf()
params[AnalyticsParam.ACTION] = action.key
params[AnalyticsParam.ERROR_CODE] = error.code.toString()
params[AnalyticsParam.ERROR_DESCRIPTION] = "${error.javaClass.simpleName}: ${error.customMessage}"
params[AnalyticsParam.ERROR_KEY] = "BlockchainSdkError"
FirebaseCrashlytics.getInstance().apply {
params.forEach { setCustomKey(it.key.param, it.value) }
recordException(error)
}
}
}
}
}
}

View file

@ -27,6 +27,7 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_round
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round
Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_round
Blockchain.Gnosis -> R.drawable.ic_gnosis_round
else -> R.drawable.ic_tangem_logo
}
}
@ -53,6 +54,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_no_color
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color
Blockchain.Tron, Blockchain.TronTestnet -> R.drawable.ic_tron_no_color
Blockchain.Gnosis -> R.drawable.ic_gnosis_no_color
else -> R.drawable.ic_tangem_logo
}
}

View file

@ -7,11 +7,13 @@ import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.DetailsConfirmFragment
import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.details.ui.DetailsSecurityFragment
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectSessionsFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
@ -61,7 +63,11 @@ fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
}
fun FragmentActivity.getPreviousScreen(): AppScreen? {
val indexOfLastFragment = this.supportFragmentManager.backStackEntryCount - 1
val indexOfLastFragment = if (this.supportFragmentManager.backStackEntryCount > 0) {
this.supportFragmentManager.backStackEntryCount - 1
} else {
0
}
val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount)
this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name
else null
@ -88,13 +94,15 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
AppScreen.DetailsSecurity -> SecurityModeFragment()
AppScreen.CardSettings -> CardSettingsFragment()
AppScreen.AppSettings -> AppSettingsFragment()
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment()
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()
}
}

View file

@ -116,6 +116,8 @@ fun BigDecimal.formatAmountAsSpannedString(
val integer = amount.substringBefore('.')
val reminder = amount.substringAfter('.')
// test formatter log Log.e("TEST ", BigDecimal("1234567890987654321.1234567890987654321").formatWithSpaces())
return buildSpannedString {
append(integer)
append('.')
@ -136,7 +138,7 @@ fun BigDecimal.formatWithSpaces(): String {
var index: Int = integerStr.length
while (0 < index) {
if (index <= 3) {
packets.add(0, integerStr)
packets.add(integerStr)
break
}
index -= 3
@ -145,7 +147,7 @@ fun BigDecimal.formatWithSpaces(): String {
}
return buildString {
packets.forEachIndexed { index, packet ->
packets.reversed().forEachIndexed { index, packet ->
append(packet)
if (index != packets.lastIndex) append(' ')
}

View file

@ -19,4 +19,6 @@ fun StringBuilder.appendIfNotNull(value: String?, prefix: String? = null, postfi
fun String.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): String {
return StringBuilder(this).apply { appendIfNotNull(value, prefix, postfix) }.toString()
}
}
fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count))

View file

@ -93,32 +93,39 @@ fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> U
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun Context.pixelsToDp(pixels: Int): Int {
return (pixels.toFloat() /
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
.toInt()
}
fun Context.dpToPixels(dp: Float): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.resources.displayMetrics)
fun Context.pixelsToDp(pixels: Float): Float =
(pixels / (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
)
this.layoutParams = params
}
@ -128,11 +135,11 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
val flags = this.window.decorView.systemUiVisibility
// Update the SystemUiVisibility dependening on whether we want a Light or Dark theme.
this.window.decorView.systemUiVisibility =
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
@ -150,7 +157,7 @@ fun Context.copyToClipboard(value: Any, label: String = "") {
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
?: return default
?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default
@ -172,10 +179,10 @@ fun Fragment.shareText(text: String) {
}
fun Context.safeStartActivity(
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
) {
try {
this.startActivity(intent, options)
@ -187,7 +194,7 @@ fun Context.safeStartActivity(
}
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, formatArgs)
return context.getString(resId, *formatArgs)
}
fun View.animateVisibility(

View file

@ -8,7 +8,7 @@ import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.NetworkConnectivity
@ -21,7 +21,9 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
if (isDemoWallet() || TestActions.testAmountInjectionForWalletManagerEnabled) {
val scanResponse = store.state.globalState.scanResponse
if (scanResponse?.isDemoCard() == true || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)
@ -49,11 +51,10 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
fun WalletManager?.getToUpUrl(): String? {
val globalState = store.state.globalState
val exchangeManager = globalState.exchangeManager ?: return null
val wallet = this?.wallet ?: return null
val defaultAddress = wallet.address
return exchangeManager.getUrl(
return globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,
cryptoCurrencyName = wallet.blockchain.currency,

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common.feature
/**
[REDACTED_AUTHOR]
*/
interface Feature {
fun featureIsSwitchedOn():Boolean
}

View file

@ -0,0 +1,127 @@
package com.tangem.tap.common.feedback
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.stripZeroPlainString
class AdditionalFeedbackInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var address: String = "",
var explorerLink: String = "",
var host: String = "",
var derivationPath: String = "",
)
var appVersion: String = ""
// card
var cardId: String = ""
var cardFirmwareVersion: String = ""
var cardIssuer: String = ""
var cardBlockchain: String = ""
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
var signedHashesCount: String = ""
// device
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
// send error
var destinationAddress: String = ""
var amount: String = ""
var fee: String = ""
var token: String = ""
fun setAppVersion(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
fun setCardInfo(data: ScanResponse) {
cardId = data.card.cardId
cardBlockchain = data.walletData?.blockchain ?: ""
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = data.card.wallets
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()
walletManagers.forEach { manager ->
walletsInfo.add(
EmailWalletInfo(
blockchain = manager.wallet.blockchain,
address = getAddress(manager.wallet),
explorerLink = getExploreUri(manager.wallet),
host = manager.currentHost,
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
)
)
if (manager.cardTokens.isNotEmpty()) {
tokens[manager.wallet.blockchain] = manager.cardTokens
}
}
}
fun updateOnSendError(
wallet: Wallet,
host: String,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
) {
onSendErrorWalletInfo = EmailWalletInfo(
blockchain = wallet.blockchain,
address = getAddress(wallet),
explorerLink = getExploreUri(wallet),
host = host,
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
)
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
}
private fun getAddress(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.address
} else {
val addresses = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${it.value}"
}
"Multiple address: $addresses"
}
}
private fun getExploreUri(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.getExploreUrl(wallet.address)
} else {
val links = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
}
"Multiple explorers links: $links"
}
}
}

View file

@ -0,0 +1,109 @@
package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.extensions.breakLine
import com.tangem.wallet.R
interface FeedbackData {
val subjectResId: Int
val mainMessageResId: Int
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
fun prepare(infoHolder: AdditionalFeedbackInfo) {}
fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String
fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
breakLine(3)
append(context.getString(getDataCollectionMessageResId()))
breakLine()
append(createOptionalMessage(infoHolder))
}.toString()
}
}
class RateCanBeBetterEmail : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_rate_negative
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.breakLine()
.appendPhoneInfo()
.build()
}
class ScanFailsEmail : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_scan_failed
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
override fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String = StringBuilder().apply {
append(context.getString(mainMessageResId))
breakLine(4)
append(createOptionalMessage(infoHolder))
}.toString()
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendPhoneInfo()
.build()
}
class SendTransactionFailedEmail(
val error: String,
) : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_tx_failed
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendDelimiter()
.appendTxFailedBlockchainInfo(error)
.breakLine()
.appendPhoneInfo()
.build()
}
class FeedbackEmail : FeedbackData {
override val subjectResId: Int
get() = if (isS2CCard) s2cSubject else tangemSubject
override val mainMessageResId: Int
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
private val tangemMainMessage: Int = R.string.feedback_preface_support
private val s2cSubject: Int = R.string.feedback_subject_support
private val s2cMainMessage: Int = R.string.feedback_preface_support
private var isS2CCard = false
override fun prepare(infoHolder: AdditionalFeedbackInfo) {
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
}
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendWalletsInfo()
.breakLine()
.appendPhoneInfo()
.build()
}
class SupportInfo : FeedbackData {
override val subjectResId: Int = R.string.details_ask_a_question
override val mainMessageResId: Int = R.string.details_ask_a_question
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String {
return FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendDelimiter()
.appendPhoneInfo()
.build()
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.tap.common.feedback
import com.tangem.tap.common.extensions.breakLine
class FeedbackDataBuilder(
private val infoHolder: AdditionalFeedbackInfo
) {
val builder = StringBuilder()
fun appendDelimiter(): FeedbackDataBuilder {
builder.appendDelimiter()
return this
}
fun breakLine(count: Int = 1): FeedbackDataBuilder {
builder.breakLine(count)
return this
}
fun appendCardInfo(): FeedbackDataBuilder {
builder.appendKeyValue("Card ID", infoHolder.cardId)
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
return this
}
fun appendWalletsInfo(): FeedbackDataBuilder {
infoHolder.walletsInfo.forEach {
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Explorer link", it.explorerLink)
infoHolder.tokens[it.blockchain]?.let { tokens ->
builder.append("Tokens:")
breakLine()
tokens.forEach { token ->
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Contract address", token.contractAddress)
}
}
}
return this
}
fun appendTxFailedBlockchainInfo(error: String): FeedbackDataBuilder {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalFeedbackInfo.EmailWalletInfo()
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
builder.appendKeyValue("Host", walletInfo.host)
builder.appendKeyValue("Token", infoHolder.token)
builder.appendKeyValue("Error", error)
builder.appendDelimiter()
builder.appendKeyValue("Source address", walletInfo.address)
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
builder.appendKeyValue("Amount", infoHolder.amount)
builder.appendKeyValue("Fee", infoHolder.fee)
return this
}
fun appendPhoneInfo(): FeedbackDataBuilder {
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)
builder.appendKeyValue("App version", infoHolder.appVersion)
return this
}
fun build(): String = builder.toString()
}
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
return if (value.isNotBlank()) this.append("$key: $value\n") else this
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")

View file

@ -0,0 +1,143 @@
package com.tangem.tap.common.feedback
import android.content.Context
import android.os.Build
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.logConfig
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import com.zendesk.logger.Logger
import timber.log.Timber
import zendesk.chat.Chat
import zendesk.chat.ChatConfiguration
import zendesk.chat.ChatEngine
import zendesk.chat.ChatProvidersConfiguration
import zendesk.chat.VisitorInfo
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
/**
[REDACTED_AUTHOR]
*/
class FeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val preferencesStorage: PreferencesStorage,
) {
fun initChat(
context: Context,
zendeskConfig: ZendeskConfig,
) {
Chat.INSTANCE.init(
context,
zendeskConfig.accountKey,
zendeskConfig.appId
)
// Zendesk logs
Logger.setLoggable(logConfig.zendesk)
}
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
val fileLog = if (feedbackData is ScanFailsEmail) createLogFile(activity) else null
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = fileLog,
onFail = onFail
)
}
}
fun openChat(feedbackData: FeedbackData) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
setChatVisitorInfo()
setChatVisitorNote(activity, feedbackData)
showMessagingActivity(activity)
}
}
private fun createLogFile(context: Context): File? {
return try {
val file = File(context.filesDir, "logs.txt")
file.delete()
file.createNewFile()
val stringWriter = StringWriter()
logCollector.getLogs().forEach { stringWriter.append(it) }
val fileWriter = FileWriter(file)
fileWriter.write(stringWriter.toString())
fileWriter.close()
logCollector.clearLogs()
file
} catch (ex: Exception) {
Timber.e(ex, "Can't create the logs file")
null
}
}
private fun setChatVisitorInfo() {
if (preferencesStorage.chatFirstLaunchTime == null) {
preferencesStorage.chatFirstLaunchTime = System.currentTimeMillis()
}
val chatUserId = (preferencesStorage.chatFirstLaunchTime.toString() + Build.MODEL).hashCode()
val visitorInfo = VisitorInfo.builder()
.withName("User $chatUserId")
.build()
Chat.INSTANCE.chatProvidersConfiguration = ChatProvidersConfiguration.builder()
.withVisitorInfo(visitorInfo)
.build()
}
private fun setChatVisitorNote(
context: Context,
feedbackData: FeedbackData,
) {
Chat.INSTANCE.providers()
?.profileProvider()
?.setVisitorNote(feedbackData.joinTogether(context, infoHolder))
}
private fun showMessagingActivity(context: Context) {
MessagingActivity.builder()
.withMultilineResponseOptionsEnabled(false)
.withBotLabelStringRes(R.string.chat_bot_name)
.withBotAvatarDrawable(R.mipmap.ic_launcher)
.withEngines(ChatEngine.engine())
.show(context, buildChatConfig())
}
private fun buildChatConfig(): Configuration {
return ChatConfiguration.builder()
.withOfflineFormEnabled(true)
.withAgentAvailabilityEnabled(true)
.withPreChatFormEnabled(false)
.build()
}
private fun getSupportEmail(): String {
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
S2C_SUPPORT_EMAIL
} else {
DEFAULT_SUPPORT_EMAIL
}
}
companion object {
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.common.log
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import java.text.SimpleDateFormat
import java.util.*
class TangemLogCollector(
private val levels: List<Log.Level>,
private val messageFormatter: LogFormat,
) : TangemSdkLogger {
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
private val logs = mutableListOf<String>()
private val mutex = Object()
override fun log(message: () -> String, level: Log.Level) {
if (!levels.contains(level)) return
synchronized(mutex) {
val formattedMessage = messageFormatter.format(message, level)
val logMessage = "${dateFormatter.format(Date())}: $formattedMessage"
logs.add("$logMessage\n")
}
}
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
fun clearLogs() = synchronized(mutex) { logs.clear() }
}

View file

@ -2,8 +2,8 @@ package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.TestAction
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Currency
/**
[REDACTED_AUTHOR]

View file

@ -7,14 +7,18 @@ import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.*
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -73,7 +77,8 @@ sealed class GlobalAction : Action {
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction()
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
object ExchangeManager : GlobalAction() {

View file

@ -15,6 +15,7 @@ import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.exchangeServices.CardExchangeRules
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
@ -23,11 +24,11 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import java.util.Locale
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import java.util.*
class GlobalMiddleware {
companion object {
@ -62,9 +63,11 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
))
store.dispatch(
GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency(),
),
)
}
is GlobalAction.HideWarningMessage -> {
store.state.globalState.warningManager?.let {
@ -79,8 +82,11 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
}
}
is GlobalAction.SendFeedback -> {
store.state.globalState.feedbackManager?.send(action.emailData)
is GlobalAction.SendEmail -> {
store.state.globalState.feedbackManager?.sendEmail(action.feedbackData)
}
is GlobalAction.OpenChat -> {
store.state.globalState.feedbackManager?.openChat(action.feedbackData)
}
is GlobalAction.UpdateWalletSignedHashes -> {
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
@ -104,7 +110,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
secret = mercuryoSecret,
)
val sellService = MoonPayService(moonPayKey, moonPaySecretKey)
val exchangeManager = CurrencyExchangeManager(buyService, sellService)
val cardProvider = { store.state.globalState.scanResponse?.card }
val exchangeManager = CurrencyExchangeManager(
buyService = buyService,
sellService = sellService,
primaryRules = CardExchangeRules(cardProvider),
)
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
}
@ -124,7 +136,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
store.state.globalState.analyticsHandlers,
currenciesRepository,
action.additionalBlockchainsToDerive,
action.messageResId
action.messageResId,
)
withMainContext {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
@ -147,15 +159,15 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
is Result.Success -> {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = result.data.code.lowercase()
)
countryCode = result.data.code.lowercase(),
),
)
}
is Result.Failure -> {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase()
)
countryCode = Locale.getDefault().country.lowercase(),
),
)
}
}

View file

@ -3,12 +3,12 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
@ -25,7 +25,7 @@ data class GlobalState(
val appCurrency: FiatCurrency = FiatCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val resources: AndroidResources = AndroidResources(),
val analyticsHandlers: AnalyticsHandler? = null,
val userCountryCode: String? = null,

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.navigation
import android.net.Uri
import androidx.fragment.app.FragmentActivity
import org.rekotlin.Action
import java.lang.ref.WeakReference
@ -15,6 +16,8 @@ sealed class NavigationAction : Action {
data class OpenUrl(val url: String) : NavigationAction()
data class OpenDocument(val url: Uri) : NavigationAction()
data class Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.navigation
import android.content.Intent
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
@ -32,6 +33,11 @@ val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
CustomTabsManager().openUrl(action.url, it)
}
}
is NavigationAction.OpenDocument -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
is NavigationAction.Share -> {
navState?.activity?.get()?.let {
it.shareText(action.data)

View file

@ -16,7 +16,7 @@ enum class AppScreen {
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,
Send,
Details, DetailsConfirm, DetailsSecurity,
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory,
AddTokens, AddCustomToken,
WalletConnectSessions,
QrScan

View file

@ -5,6 +5,7 @@ import android.content.Intent
import com.google.android.gms.wallet.PaymentData
import com.shopify.buy3.Storefront
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.common.shop.data.TotalSum
@ -21,7 +22,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private val shopifyService = ShopifyService(application, shopifyShop)
private lateinit var product: Storefront.Product
private val checkouts = mutableMapOf<ProductType, Storefront.Checkout>()
private val variants = mutableMapOf<ProductType, Storefront.ProductVariant>()
@ -31,22 +31,19 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
suspend fun getProducts(): Result<List<TangemProduct>> {
val result = shopifyService.getProducts()
result.onSuccess {
product = it.first {
val variantsSku = it.variants.edges.map { it.node.sku }
variantsSku.contains(ProductType.WALLET_2_CARDS.sku) && variantsSku.contains(
ProductType.WALLET_3_CARDS.sku
)
}
product.variants.edges.map { it.node }
.forEach { variant ->
if (variant.sku == ProductType.WALLET_2_CARDS.sku) {
variants[ProductType.WALLET_2_CARDS] = variant
} else if (variant.sku == ProductType.WALLET_3_CARDS.sku) {
variants[ProductType.WALLET_3_CARDS] = variant
}
}
return result.mapCatching { product ->
val availableVariants = product
.flatMap { it.variants.edges.map { it.node } }
.filter { SKUS_TO_DISPLAY.contains(it.sku) }
.associateBy { ProductType.fromSku(it.sku) }
.filterNotNull()
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 }}"))
}
val twoCardsProduct = TangemProduct(
type = ProductType.WALLET_2_CARDS,
@ -65,7 +62,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
createCheckouts()
return Result.success(listOf(twoCardsProduct, threeCardsProduct))
}
return Result.failure(result.exceptionOrNull()!!)
}
private suspend fun createCheckouts() {
@ -214,6 +210,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
companion object {
const val TANGEM_WALLET_2_CARDS_SKU = "TG115x2"
const val TANGEM_WALLET_3_CARDS_SKU = "TG115x3"
val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU)
}
}

View file

@ -4,5 +4,15 @@ import com.tangem.tap.common.shop.TangemShopService
enum class ProductType(val sku: String) {
WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU),
WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU)
WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU);
companion object {
fun fromSku(sku: String): ProductType? {
return when (sku) {
WALLET_2_CARDS.sku -> WALLET_2_CARDS
WALLET_3_CARDS.sku -> WALLET_3_CARDS
else -> null
}
}
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
class SimpleAlertDialog {
companion object {
fun create(
titleRes: Int? = null,
messageRes: Int? = null,
title: String? = null,
message: String? = null,
primaryButtonRes: Int = R.string.common_ok,
context: Context,
): AlertDialog {
return SimpleCancelableAlertDialog.create(
titleRes = titleRes,
messageRes = messageRes,
title = title,
message = message,
primaryButtonRes = primaryButtonRes,
secondaryButtonRes = null,
context = context
)
}
}
}
class SimpleCancelableAlertDialog {
companion object {
fun create(
titleRes: Int? = null,
messageRes: Int? = null,
title: String? = null,
message: String? = null,
primaryButtonRes: Int = R.string.common_ok,
secondaryButtonRes: Int? = R.string.common_cancel,
primaryButtonAction: () -> Unit = {},
secondaryButtonAction: () -> Unit = {},
context: Context,
): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(titleRes?.let { context.getString(it) } ?: title )
setMessage(messageRes?.let { context.getString(it) } ?: message)
setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() }
if (secondaryButtonRes != null) {
setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction()}
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.common.zendesk
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ZendeskConfig(
@Json(name = "zendeskApiKey")
val apiKey: String,
@Json(name = "zendeskAppId")
val appId: String,
@Json(name = "zendeskClientId")
val clientId: String,
@Json(name = "zendeskAccountKey")
val accountKey: String,
@Json(name = "zendeskUrl")
val url: String,
)

View file

@ -3,7 +3,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.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import java.math.BigDecimal

View file

@ -19,6 +19,7 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.ScanResponse
import com.tangem.operations.CommandResponse
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.CheckUserCodesCommand
@ -144,6 +145,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
}
suspend fun scanCard(): CompletionResult<Card> {
return runTaskAsyncReturnOnMain(
ScanTask(),
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
}
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> =

View file

@ -5,44 +5,25 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.tasks.SignHashTask
import com.tangem.common.card.Card
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
private val card: Card,
private val tangemSdk: TangemSdk,
private val initialMessage: Message,
private val signerCallback: (TangemSignerResponse) -> Unit,
) : TransactionSigner {
override suspend fun sign(hash: ByteArray, cardId: String, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
override suspend fun sign(
hashes: List<ByteArray>,
publicKey: Wallet.PublicKey
): CompletionResult<List<ByteArray>> {
return suspendCoroutine { continuation ->
val command = SignHashTask(hash, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = command,
cardId = cardId,
initialMessage = initialMessage,
) { result ->
when (result) {
is CompletionResult.Success -> {
signerCallback(
TangemSignerResponse(
result.data.totalSignedHashes,
result.data.remainingSignatures
)
)
continuation.resume(CompletionResult.Success(result.data.signature))
}
is CompletionResult.Failure ->
continuation.resume(CompletionResult.Failure(result.error))
}
}
}
}
val cardId = if (card.backupStatus?.isActive == true) null else card.cardId
override suspend fun sign(hashes: List<ByteArray>, cardId: String, publicKey: Wallet.PublicKey): CompletionResult<List<ByteArray>> {
return suspendCoroutine { continuation ->
val task = SignHashesTask(hashes, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = task,
@ -65,6 +46,21 @@ class TangemSigner(
}
}
}
override suspend fun sign(
hash: ByteArray,
publicKey: Wallet.PublicKey
): CompletionResult<ByteArray> {
val result = sign(
hashes = listOf(hash),
publicKey = publicKey
)
return when (result) {
is CompletionResult.Success -> CompletionResult.Success(result.data.first())
is CompletionResult.Failure -> CompletionResult.Failure(result.error)
}
}
}
data class TangemSignerResponse(

View file

@ -39,21 +39,22 @@ sealed class TapError(
object DustChange : TapError(R.string.send_error_dust_change)
data class CreateAccountUnderfunded(override val args: List<Any>) : TapError(R.string.send_error_no_target_account)
sealed class XmlError {
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
}
data class UnsupportedState(
val stateError: String,
val customMessage: String = "Unsupported state:"
) : TapError(R.string.common_custom_string, listOf("$customMessage $stateError"))
sealed class WalletManager {
object CreationError: CustomError("Can't create wallet manager")
class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount)
class InternalError(message: String): CustomError(message)
object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable)
object BlockchainIsUnreachableTryLater: TapError(R.string.wallet_balance_blockchain_unreachable_try_later)
object CreationError : CustomError("Can't create wallet manager")
class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount)
class InternalError(message: String) : CustomError(message)
object BlockchainIsUnreachable : TapError(R.string.wallet_balance_blockchain_unreachable)
object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later)
}
data class ValidateTransactionErrors(
override val errorList: List<TapError>,
override val builder: (List<String>) -> String
override val errorList: List<TapError>,
override val builder: (List<String>) -> String
) : TapError(-1), MultiMessageError
}

View file

@ -1,6 +1,12 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkConfig
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.services.Result
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
@ -26,7 +32,7 @@ import kotlinx.coroutines.withContext
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
by lazy { WalletManagerFactory(blockchainSdkConfig) }
val rates: RatesRepository = RatesRepository()
@ -34,10 +40,10 @@ class TapWalletManager {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler = ThrottlerWithValues<BlockchainNetwork, Result<Wallet>>(10000)
private val walletManagersThrottler =
ThrottlerWithValues<BlockchainNetwork, Result<Wallet>>(10000)
suspend fun loadWalletData(walletManager: WalletManager) {
val blockchain = walletManager.wallet.blockchain
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
val result = if (walletManagersThrottler.isStillThrottled(blockchainNetwork)) {
walletManagersThrottler.geValue(blockchainNetwork)!!
@ -54,17 +60,21 @@ class TapWalletManager {
is Result.Failure -> {
when (result.error) {
is TapError.WalletManager.NoAccountError -> {
dispatchOnMain(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManager.NoAccountError).customMessage
))
dispatchOnMain(
WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManager.NoAccountError).customMessage
)
)
}
else -> {
dispatchOnMain(WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
))
dispatchOnMain(
WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
)
)
}
}
}
@ -77,10 +87,16 @@ class TapWalletManager {
updateConfigManager(data)
withMainContext {
store.dispatch(WalletAction.ResetState)
store.dispatch(WalletAction.ResetState(data.card.cardId))
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
store.dispatch(
WalletAction.MultiWallet.ShowWalletBackupWarning(
show = data.card.settings.isBackupAllowed
&& data.card.backupStatus == Card.BackupStatus.NoBackup
)
)
loadData(data)
}
}
@ -109,6 +125,37 @@ class TapWalletManager {
return
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data)
} else {
loadSingleWalletData(data)
}
dispatchOnMain(WalletAction.LoadWallet())
dispatchOnMain(WalletAction.LoadFiatRate())
}
private suspend fun loadMultiWalletData(
scanResponse: ScanResponse
) {
val savedCurrencies = currenciesRepository.loadSavedCurrencies(
scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed
)
if (savedCurrencies.isEmpty()) return
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies)
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
)
savedCurrencies.map {
if (it.tokens.isNotEmpty()) {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
}
}
}
private suspend fun loadSingleWalletData(data: ScanResponse) {
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
@ -120,87 +167,12 @@ class TapWalletManager {
primaryWalletManager.addToken(primaryToken)
dispatchOnMain(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, primaryWalletManager)
} else {
dispatchOnMain(WalletAction.MultiWallet.AddBlockchains(
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
listOf(primaryWalletManager)
))
}
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, null)
}
}
dispatchOnMain(WalletAction.LoadWallet())
dispatchOnMain(WalletAction.LoadFiatRate())
}
private suspend fun loadMultiWalletData(
scanResponse: ScanResponse,
primaryBlockchain: Blockchain?,
primaryWalletManager: WalletManager?
) {
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
val savedCurrencies = currenciesRepository.loadSavedCurrencies(
scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed
)
if (savedCurrencies.isEmpty()) {
if (primaryBlockchain != null && primaryWalletManager != null) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(primaryWalletManager)
dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(listOf(blockchainNetwork)),
WalletAction.MultiWallet.AddBlockchains(
listOf(blockchainNetwork),
listOf(primaryWalletManager)
),
WalletAction.MultiWallet.AddTokens(primaryTokens.toList(), blockchainNetwork)
)
} else {
val blockchainNetworks = listOf(
BlockchainNetwork(Blockchain.Bitcoin, scanResponse.card),
BlockchainNetwork(Blockchain.Ethereum, scanResponse.card)
)
val walletManagers = walletManagerFactory.makeWalletManagersForApp(
scanResponse,
blockchainNetworks
)
dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks),
WalletAction.MultiWallet.AddBlockchains(blockchainNetworks, walletManagers),
)
}
// dispatchOnMain(
// WalletAction.MultiWallet.FindBlockchainsInUse,
// WalletAction.MultiWallet.FindTokensInUse,
// )
} else {
val walletManagers = if (
primaryTokens.isNotEmpty() &&
primaryWalletManager != null &&
primaryBlockchain != null && primaryBlockchain != Blockchain.Unknown
) {
val blockchainsWithoutPrimary = savedCurrencies.filterNot { it.blockchain == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(
scanResponse,
blockchainsWithoutPrimary
).plus(primaryWalletManager)
} else {
walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies)
}
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
)
savedCurrencies.map {
if (it.tokens.isNotEmpty()) {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
}
}
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.domain.configurable.Loader
/**
@ -20,7 +20,8 @@ data class Config(
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null
val shopify: ShopifyShop? = null,
val zendesk: ZendeskConfig? = null,
)
class ConfigManager(
@ -33,11 +34,11 @@ class ConfigManager(
private var defaultConfig = Config()
fun load(onComplete: VoidCallback? = null) {
localLoader.load { config ->
setupFeature(config.features)
setupKey(config.configValues)
onComplete?.invoke()
fun load(onComplete: ((config: Config) -> Unit)? = null) {
localLoader.load { configModel ->
setupFeature(configModel.features)
setupKey(configModel.configValues)
onComplete?.invoke(config)
}
// Uncomment to enable remote config
// remoteLoader.load { config ->
@ -95,6 +96,7 @@ class ConfigManager(
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
zendesk = values.zendesk,
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
@ -110,6 +112,7 @@ class ConfigManager(
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
zendesk = values.zendesk,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
/**
[REDACTED_AUTHOR]
@ -25,6 +26,7 @@ class ConfigValueModel(
val infuraProjectId: String?,
val appsFlyerDevKey: String,
val shopifyShop: ShopifyShop?,
val zendesk: ZendeskConfig?,
val tronGridApiKey: String,
)

View file

@ -6,7 +6,6 @@ import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.common.extensions.removeBy
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import java.util.*
/**
[REDACTED_AUTHOR]
@ -188,31 +187,6 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun restoreFundsWarning(): WarningMessage = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Warning,
listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.alert_title,
messageResId = R.string.alert_funds_restoration_message,
origin = WarningMessage.Origin.Local,
buttonTextId = R.string.warning_button_learn_more
)
const val REMAINING_SIGNATURES_WARNING = 10
private const val RESTORE_FUNDS_GUIDE_URL_RU =
"https://tangem.com/ru/kak-vosstanovit-tokeny-otpravlennye-ne-na-tot-adres-v-tangem-wallet"
private const val RESTORE_FUNDS_GUIDE_URL_EN =
"https://tangem.com/en/how-to-recover-crypto-sent-to-the-wrong-address-in-tangem-wallet"
fun getRestoreFundsGuideUrl(locale: String): String {
return if (locale == Locale("ru").language) {
RESTORE_FUNDS_GUIDE_URL_RU
} else {
RESTORE_FUNDS_GUIDE_URL_EN
}
}
}
}

View file

@ -6,8 +6,8 @@ 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.TapWorkarounds
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.getTwinCardNumber
import com.tangem.domain.common.isTangemTwin
@ -15,7 +15,6 @@ 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
@ -24,7 +23,7 @@ val Card.isWalletDataSupported: Boolean
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTangemTwin() && !isStart2Coin && !TapWorkarounds.isTangemNote(this)
return !isTangemTwin() && !isStart2Coin && !isTangemNote()
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -11,7 +11,7 @@ import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.models.Currency
fun WalletManagerFactory.makeWalletManagerForApp(
scanResponse: ScanResponse,
@ -32,9 +32,10 @@ fun WalletManagerFactory.makeWalletManagerForApp(
return when {
scanResponse.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> {
makeTwinWalletManager(
card.cardId,
wallet.publicKey, scanResponse.secondTwinPublicKey!!.hexToBytes(),
environmentBlockchain, wallet.curve
walletPublicKey = wallet.publicKey,
pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(),
blockchain = environmentBlockchain,
curve = wallet.curve
)
}
seedKey != null && derivationParams != null -> {
@ -47,7 +48,6 @@ fun WalletManagerFactory.makeWalletManagerForApp(
?: return null
makeWalletManager(
cardId = card.cardId,
blockchain = environmentBlockchain,
seedKey = wallet.publicKey,
derivedKey = derivedKey,
@ -56,7 +56,6 @@ fun WalletManagerFactory.makeWalletManagerForApp(
}
else -> {
makeWalletManager(
cardId = card.cardId,
blockchain = environmentBlockchain,
walletPublicKey = wallet.publicKey,
curve = wallet.curve

View file

@ -57,7 +57,6 @@ class WalletStateConverter : StringStateConverter<AppState> {
walletMap["publicKey"] = publicKeyMap
walletMap["amounts"] = amounts
walletMap["addresses"] = wallet.addresses.toString()
walletMap["cardId"] = wallet.cardId
return walletMap
}

View file

@ -14,10 +14,14 @@ 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.*
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isExcluded
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.common.isTangemTwins
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.ScanTask
@ -56,7 +60,7 @@ class ScanProductTask(
}
val commandProcessor = when {
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
else -> ScanWalletProcessor(currenciesRepository, additionalBlockchainsToDerive)
}
@ -64,16 +68,15 @@ class ScanProductTask(
when (processorResult) {
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
is CompletionResult.Success -> callback(
CompletionResult.Success(
processorResult.data
is CompletionResult.Success -> {
// it need because processorResult.data.card doesn't contains attestation result
// and CardWallet.derivedKeys
val processorScanResponseWithNewCard = processorResult.data.copy(
card = scanTaskResult.data
)
)
is CompletionResult.Failure -> callback(
CompletionResult.Failure(
scanTaskResult.error
)
)
callback(CompletionResult.Success(processorScanResponseWithNewCard))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error))
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(processorResult.error))
@ -98,11 +101,11 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
callback(
CompletionResult.Success(
ScanResponse(
card,
ProductType.Note,
session.environment.walletData
)
)
card = card,
productType = ProductType.Note,
walletData = session.environment.walletData,
),
),
)
}
}
@ -113,19 +116,17 @@ private class ScanWalletProcessor(
) : ProductCommandProcessor<ScanResponse> {
var primaryCard: PrimaryCard? = null
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
createMissingWalletsIfNeeded(card, session, callback)
}
private fun createMissingWalletsIfNeeded(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
startLinkingForBackupIfNeeded(card, session, callback)
@ -142,18 +143,12 @@ private class ScanWalletProcessor(
when (result) {
is CompletionResult.Success -> {
PreflightReadTask(
PreflightReadMode.FullCardRead,
card.cardId
readMode = PreflightReadMode.FullCardRead,
cardId = card.cardId
).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
startLinkingForBackupIfNeeded(card, session, callback)
}
is CompletionResult.Failure -> callback(
CompletionResult.Failure(
readResult.error
)
)
is CompletionResult.Success -> startLinkingForBackupIfNeeded(card, session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
@ -161,18 +156,14 @@ private class ScanWalletProcessor(
}
}
}
private fun startLinkingForBackupIfNeeded(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val activationIsFinished =
preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId)
val activationIsFinished = preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId)
if (card.backupStatus == Card.BackupStatus.NoBackup &&
!activationIsFinished && card.wallets.isNotEmpty()
) {
if (card.backupStatus == Card.BackupStatus.NoBackup && !activationIsFinished && card.wallets.isNotEmpty()) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
@ -188,11 +179,10 @@ private class ScanWalletProcessor(
deriveKeysIfNeeded(card, session, callback)
}
}
private fun deriveKeysIfNeeded(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
scope.launch {
val derivations = collectDerivations(card)
@ -231,12 +221,13 @@ private class ScanWalletProcessor(
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val currenciesRepository = currenciesRepository ?: return emptyList()
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val cardCurrencies = currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val blockchainsToDerive = cardCurrencies.ifEmpty {
mutableListOf(
BlockchainNetwork(Blockchain.Bitcoin, card),
BlockchainNetwork(Blockchain.Ethereum, card)
BlockchainNetwork(Blockchain.Ethereum, card),
)
}
@ -244,7 +235,7 @@ private class ScanWalletProcessor(
blockchainsToDerive.addAll(
listOf(
BlockchainNetwork(Blockchain.Ethereum, card),
BlockchainNetwork(Blockchain.EthereumTestnet, card)
BlockchainNetwork(Blockchain.EthereumTestnet, card),
)
)
}
@ -301,52 +292,44 @@ private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(
CompletionResult.Success(
ScanResponse(
card,
ProductType.Twins,
null
)
)
)
return@run
}
val verified =
TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletData = session.environment.walletData
val response = ScanResponse(
card,
ProductType.Twins,
walletData,
twinPublicKey.toHexString()
card = card,
productType = ProductType.Twins,
walletData = null,
)
callback(CompletionResult.Success(response))
return@run
}
val verified = TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey)
val response = if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletData = session.environment.walletData
ScanResponse(
card = card,
productType = ProductType.Twins,
walletData = walletData,
secondTwinPublicKey = twinPublicKey.toHexString(),
)
} else {
callback(
CompletionResult.Success(
ScanResponse(
card,
ProductType.Twins,
null
)
)
ScanResponse(
card = card,
productType = ProductType.Twins,
walletData = null,
)
}
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure ->
is CompletionResult.Failure -> {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null)))
}
}
}
}
}
fun Card.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
val curvesPresent = wallets.map { it.curve }.toSet()
val curvesForNonCreatedWallets = supportedCurves
.subtract(curvesPresent + EllipticCurve.Secp256r1)
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
return curvesForNonCreatedWallets.toList()
}

View file

@ -4,16 +4,17 @@ import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat
import com.tangem.common.card.Card
import java.util.*
import java.util.Locale
/**
[REDACTED_AUTHOR]
*/
class CardTou {
private val locale: Locale = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)
private val locale: Locale =
ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)!!
fun getUrl(card: Card): Uri? {
val issuerName = card.issuer.name ?: return null
val issuerName = card.issuer.name
if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null
val baseUrl = "https://app.tangem.com/tou/"

View file

@ -1,11 +1,13 @@
package com.tangem.tap.domain.walletconnect
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.toMap
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import com.google.gson.annotations.SerializedName
import com.tangem.blockchain.common.Blockchain
import com.trustwallet.walletconnect.JSONRPC_VERSION
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
@ -18,9 +20,11 @@ class EthSignHelper {
.create()
}
fun tryToParseEthTypedMessage(data: String): WCEthereumSignMessage? {
val request =
gson.fromJson<CustomJsonRpcRequests>(data)
fun parseCustomRequest(data: String): CustomJsonRpcRequest {
return gson.fromJson<CustomJsonRpcRequest>(data)
}
fun tryToParseEthTypedMessage(request: CustomJsonRpcRequest): WCEthereumSignMessage? {
return if (request.method == WCMethodExtended.ETH_SIGN_TYPE_DATA_V4) {
WCEthereumSignMessage(
listOf(
@ -53,12 +57,26 @@ class EthSignHelper {
}
}
data class CustomJsonRpcRequests(
data class CustomJsonRpcRequest(
val id: Long,
val jsonrpc: String = JSONRPC_VERSION,
val method: WCMethodExtended?,
val params: JsonArray
)
) {
fun blockchainFromChainId(): Blockchain? {
return try {
val hex = params[0].asJsonObject.toMap()[CHAIN_ID_KEY]?.asString ?: ""
Blockchain.fromChainId(Integer.decode(hex))
} catch (exception: Exception) {
null
}
}
companion object {
const val CHAIN_ID_KEY = "chainId"
}
}
enum class WCMethodExtended {
@SerializedName("wc_sessionRequest")
@ -95,5 +113,10 @@ enum class WCMethodExtended {
GET_ACCOUNTS,
@SerializedName("trust_signTransaction")
SIGN_TRANSACTION;
SIGN_TRANSACTION,
@SerializedName("wallet_switchEthereumChain")
SWITCH_CHAIN,
;
}

View file

@ -5,20 +5,32 @@ import com.tangem.common.extensions.guard
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.walletConnectRepository
import com.trustwallet.walletconnect.WCClient
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.binance.*
import com.trustwallet.walletconnect.models.binance.WCBinanceCancelOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTxConfirmParam
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import com.trustwallet.walletconnect.models.session.WCSession
import com.trustwallet.walletconnect.models.session.WCSessionUpdate
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.*
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.util.*
@ -71,6 +83,22 @@ class WalletConnectManager {
}
}
fun updateBlockchain(session: WalletConnectSession) {
sessions[session.session]?.client?.updateSession(
accounts = listOfNotNull(session.getAddress()),
chainId = session.wallet.blockchain?.getChainId(),
approved = true
)
val updatedSession = sessions[session.session]?.copy(
wallet = session.wallet
)
if (updatedSession != null) {
sessions[session.session] = updatedSession
}
}
private fun setupConnectionTimeoutCheck(session: WCSession) {
scope.launch {
delay(20_000)
@ -405,27 +433,47 @@ class WalletConnectManager {
onSessionClosed(session)
}
}
client.onWalletChangeNetwork = { id: Long, chainId: Int ->
Timber.d("On WC Switch Chain")
val blockchain = Blockchain.fromChainId(chainId)
Timber.d("WC switch chainID\nNew Blockchain: $blockchain")
val session = sessions[client.session]?.toWalletConnectSession()
if (session != null) {
store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session))
}
}
client.onCustomRequest = { id: Long, data: String ->
Timber.d("Custom Request")
Timber.d(data)
val request = EthSignHelper.parseCustomRequest(data)
val message = EthSignHelper.tryToParseEthTypedMessage(data)
if (message != null) {
Timber.d("onEthSign_v4: $message")
store.state.globalState.analyticsHandlers?.logWcEvent(
Analytics.WcAnalyticsEvent.Action(
Analytics.WcAction.PersonalSign
when (request.method) {
WCMethodExtended.ETH_SIGN_TYPE_DATA_V4 -> handleTypedDataV4(request, client, id)
else -> {
Timber.d("WC: unrecognized custom request")
}
}
}
}
private fun handleTypedDataV4(request: CustomJsonRpcRequest, client: WCClient, id: Long) {
val message = EthSignHelper.tryToParseEthTypedMessage(request)
if (message != null) {
Timber.d("onEthSign_v4: $message")
store.state.globalState.analyticsHandlers?.logWcEvent(
Analytics.WcAnalyticsEvent.Action(
Analytics.WcAction.PersonalSign
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id
)
)
}
}
}
}

View file

@ -28,11 +28,15 @@ class WalletConnectNetworkUtils {
peer.name.contains("BSC") -> {
Blockchain.BSC
}
peer.url.contains("honeyswap.1hive.eth.limo") -> {
// Check if something's changed after this bug report:
// https://github.com/1Hive/honeyswap-interface/issues/83
Blockchain.Gnosis
}
else -> {
Blockchain.Ethereum
Blockchain.Ethereum
}
}
}
}
}

View file

@ -5,8 +5,18 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.CommonSigner
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.blockchain.extensions.isAscii
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
@ -15,9 +25,14 @@ import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.logSendTransactionError
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.store
@ -56,7 +71,7 @@ class WalletConnectSdkHelper {
(walletManager as? EthereumGasLoader)?.getGasPrice()) {
is Result.Success -> result.data.toBigDecimal()
is Result.Failure -> {
Timber.e(result.error)
(result.error as? Throwable)?.let { Timber.e(it) }
return null
}
null -> return null
@ -108,9 +123,8 @@ class WalletConnectSdkHelper {
)
val blockchain = session.wallet.getBlockchainForSession()
return factory.makeWalletManager(
session.wallet.cardId,
blockchain,
publicKey
blockchain = blockchain,
publicKey = publicKey
)
}
@ -124,20 +138,18 @@ class WalletConnectSdkHelper {
private suspend fun sendTransaction(data: WcTransactionData): String? {
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
signer = Signer(tangemSdk)
signer = CommonSigner(tangemSdk)
)
return when (result) {
SimpleResult.Success -> {
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
}
is SimpleResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandlers?.logCardSdkError(
error,
Analytics.ActionToLog.WalletConnectTransaction,
)
}
Timber.e(result.error)
store.state.globalState.analyticsHandlers?.logSendTransactionError(
result.error,
Analytics.ActionToLog.WalletConnectTransaction,
)
Timber.e(result.error as BlockchainSdkError)
null
}
}

View file

@ -0,0 +1,94 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.wallet.redux.WalletState
class WcWalletManagerFactory(
private val factory: WalletManagerFactory,
private val currenciesRepository: CurrenciesRepository,
) {
suspend fun getWalletManager(
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = wallet.derivationPath?.rawPath,
tokens = emptyList()
)
return if (walletState.cardId == wallet.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
val blockchainNetworkWithTokens = currenciesRepository
.loadSavedCurrencies(
cardId = wallet.cardId,
isHdWalletSupported = wallet.derivationPath != null
).firstOrNull { it == blockchainNetwork }
if (blockchainNetworkWithTokens != null) {
factory.makeWalletManager(
blockchain = blockchainToMake,
publicKey = Wallet.PublicKey(
wallet.walletPublicKey!!,
wallet.derivedPublicKey,
wallet.derivationPath
),
tokens = blockchainNetworkWithTokens.tokens,
curve = blockchainToMake.getPrimaryCurve() ?: EllipticCurve.Secp256k1
)
} else {
null
}
}
}
suspend fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val card = scanResponse.card
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card
)
return if (walletState.cardId == card.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork
)
} else {
null
}
}
}
}

View file

@ -1,13 +1,20 @@
package com.tangem.tap.features.demo
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.toBlockchainSdkError
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.wallet.redux.WalletAction
@ -36,7 +43,8 @@ object DemoHelper {
WalletAction.TradeCryptoAction.Buy::class.java,
WalletAction.TradeCryptoAction.Sell::class.java,
BackupAction.StartBackup::class.java,
WalletAction.ExploreAddress::class.java
WalletAction.ExploreAddress::class.java,
DetailsAction.ResetToFactory.Start::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
@ -471,9 +479,12 @@ class DemoTransactionSender(
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val dataToSign = randomString(32).toByteArray()
val signerResponse = signer.sign(dataToSign, walletManager.wallet.cardId, walletManager.wallet.publicKey)
val signerResponse = signer.sign(
hash = dataToSign,
publicKey = walletManager.wallet.publicKey
)
return when (signerResponse) {
is CompletionResult.Success -> SimpleResult.Failure(Exception(ID))
is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError())
is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error)
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store

View file

@ -1,10 +1,10 @@
package com.tangem.tap.features.demo
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
/**
[REDACTED_AUTHOR]
*/
fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId)
fun WalletManager.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(wallet.cardId)
fun Card.isDemoCard(): Boolean = DemoHelper.isDemoCardId(cardId)

View file

@ -3,12 +3,8 @@ package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.wallet.R
import org.rekotlin.Action
sealed class DetailsAction : Action {
@ -20,31 +16,24 @@ sealed class DetailsAction : Action {
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
data class ReCreateTwinsWallet(val number: TwinCardNumber) : DetailsAction()
object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
object Check : ResetToFactory()
object Proceed : ResetToFactory() {
object NotAllowedByCard : ResetToFactory(), NotificationAction {
override val messageResource = R.string.error_purge_prohibited
}
object NotEmpty : ResetToFactory(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
}
object Confirm : ResetToFactory()
object Cancel : ResetToFactory()
object Start : ResetToFactory()
object Proceed : ResetToFactory()
data class Confirm(val confirmed: Boolean) : ResetToFactory()
object Failure : ResetToFactory()
object Success : ResetToFactory()
}
object CreateBackup : DetailsAction()
object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val card: Card) : DetailsAction()
object ResetCardSettingsData : DetailsAction()
sealed class ManageSecurity : DetailsAction() {
data class CheckCurrentSecurityOption(val card: Card) : ManageSecurity()
data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity()
object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
object SaveChanges : ManageSecurity() {
@ -52,11 +41,12 @@ sealed class DetailsAction : Action {
object Failure : ManageSecurity()
}
data class ConfirmSelection(val option: SecurityOption) : ManageSecurity() {
object AlreadySet : ManageSecurity(), NotificationAction {
override val messageResource = R.string.details_notification_security_option_already_active
}
}
object ChangeAccessCode : ManageSecurity()
}
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(val enable: Boolean, val setting: PrivacySetting) :
AppSettings()
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()

View file

@ -1,9 +1,8 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.TangemSdkError
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.domain.common.isTangemTwins
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.dispatchNotification
@ -13,7 +12,7 @@ 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.currenciesRepository
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
@ -23,44 +22,74 @@ import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, _ ->
private val managePrivacyMiddleware = ManagePrivacyMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, state ->
{ next ->
{ action ->
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
handleAction(state, action)
next(action)
}
}
}
private fun handleAction(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
val uri = store.state.detailsState.cardTermsOfUseUrl
if (uri != null) {
store.dispatch(NavigationAction.OpenDocument(uri))
}
}
is DetailsAction.ReCreateTwinsWallet -> {
val wallet =
store.state.walletState.walletManagers.map { it.wallet }.firstOrNull()
if (wallet == null) {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
} else {
if (wallet.hasSendableAmountsOrPendingTransactions()) {
val walletIsNotEmpty =
store.state.globalState.resources.strings.walletIsNotEmpty
store.dispatchNotification(walletIsNotEmpty)
} else {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
is DetailsAction.ReCreateTwinsWallet -> {
val wallet = store.state.walletState.walletManagers.map { it.wallet }.firstOrNull()
if (wallet == null) {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
} else {
if (wallet.hasSendableAmountsOrPendingTransactions()) {
val walletIsNotEmpty = store.state.globalState.resources.strings.walletIsNotEmpty
store.dispatchNotification(walletIsNotEmpty)
} else {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
}
}
is DetailsAction.CreateBackup -> {
store.state.detailsState.scanResponse?.let {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(
GlobalAction.Onboarding.Start(
it,
fromHomeScreen = false,
),
)
}
}
DetailsAction.ScanCard -> {
scope.launch {
when (val result = tangemSdkManager.scanCard()) {
is CompletionResult.Success -> {
val card = result.data
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
}
}
is DetailsAction.CreateBackup -> {
store.state.detailsState.scanResponse?.let {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false))
is CompletionResult.Failure -> {
}
}
}
next(action)
}
}
}
@ -68,23 +97,15 @@ class DetailsMiddleware {
class EraseWalletMiddleware {
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
is DetailsAction.ResetToFactory.Start -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
}
is DetailsAction.ResetToFactory.Proceed -> {
when (store.state.detailsState.eraseWalletState) {
EraseWalletState.Allowed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm))
EraseWalletState.NotAllowedByCard ->
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotAllowedByCard)
EraseWalletState.NotEmpty ->
store.dispatch(DetailsAction.ResetToFactory.Proceed.NotEmpty)
else -> { /* no-op */
}
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins()) {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
}
}
is DetailsAction.ResetToFactory.Cancel -> {
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.ResetToFactory.Confirm -> {
val card = store.state.detailsState.scanResponse?.card ?: return
scope.launch {
val result = tangemSdkManager.resetToFactorySettings(card)
withContext(Dispatchers.Main) {
@ -98,7 +119,7 @@ class DetailsMiddleware {
store.state.globalState.analyticsHandlers?.logCardSdkError(
error,
Analytics.ActionToLog.PurgeWallet,
card = store.state.detailsState.scanResponse?.card
card = store.state.detailsState.scanResponse?.card,
)
}
}
@ -115,44 +136,13 @@ class DetailsMiddleware {
class ManageSecurityMiddleware {
fun handle(action: DetailsAction.ManageSecurity) {
when (action) {
is DetailsAction.ManageSecurity.CheckCurrentSecurityOption -> {
if (action.card.firmwareVersion >= FirmwareVersion.IsAccessCodeStatusAvailable) {
// for a card that meets this condition, we can get these statuses from it
val simulatedResponse = CheckUserCodesResponse(
action.card.isAccessCodeSet, action.card.isPasscodeSet ?: false
)
store.dispatch(DetailsAction.ManageSecurity.SetCurrentOption(simulatedResponse))
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
} else {
scope.launch {
when (val response = tangemSdkManager.checkUserCodes(action.card.cardId)) {
is CompletionResult.Success -> {
store.dispatchOnMain(DetailsAction.ManageSecurity.SetCurrentOption(response.data))
store.dispatchOnMain(DetailsAction.ManageSecurity.OpenSecurity)
}
is CompletionResult.Failure -> {
}
}
}
}
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
}
is DetailsAction.ManageSecurity.ConfirmSelection -> {
if (action.option != store.state.detailsState.securityScreenState?.currentOption) {
if (action.option == SecurityOption.LongTap) {
store.dispatch(DetailsAction.ManageSecurity.SaveChanges)
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm))
}
} else {
store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection.AlreadySet)
}
}
is DetailsAction.ManageSecurity.SaveChanges -> {
val cardId = store.state.detailsState.scanResponse?.card?.cardId
val selectedOption = store.state.detailsState.securityScreenState?.selectedOption
val selectedOption =
store.state.detailsState.cardSettingsState?.manageSecurityState?.selectedOption
scope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
@ -166,9 +156,7 @@ class DetailsMiddleware {
selectedOption?.let {
store.dispatch(GlobalAction.UpdateSecurityOptions(it))
}
if (selectedOption != SecurityOption.LongTap) {
store.dispatch(NavigationAction.PopBackTo())
}
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure -> {
@ -178,9 +166,9 @@ class DetailsMiddleware {
actionToLog = Analytics.ActionToLog.ChangeSecOptions,
parameters = mapOf(
AnalyticsParam.NEW_SECURITY_OPTION to
(selectedOption?.name ?: "")
(selectedOption?.name ?: ""),
),
card = store.state.detailsState.scanResponse?.card
card = store.state.detailsState.scanResponse?.card,
)
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
@ -191,9 +179,25 @@ class DetailsMiddleware {
}
}
}
is DetailsAction.ManageSecurity.ChangeAccessCode -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
tangemSdkManager.setAccessCode(card.cardId)
}
}
else -> { /* no-op */
}
}
}
}
}
class ManagePrivacyMiddleware {
fun handle(action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
// TODO()
}
}
}
}
}

View file

@ -1,13 +1,14 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.card.Card
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.isTangemTwin
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.store
import org.rekotlin.Action
import java.util.*
@ -19,69 +20,95 @@ class DetailsReducer {
private fun internalReduce(action: Action, state: AppState): DetailsState {
if (action !is DetailsAction) return state.detailsState
val detailsState = state.detailsState
return when (action) {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, detailsState)
handlePrepareScreen(action)
}
is DetailsAction.PrepareCardSettingsData -> {
handlePrepareCardSettingsScreen(action.card, detailsState)
}
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
is DetailsAction.ChangeAppCurrency ->
detailsState.copy(appCurrency = action.fiatCurrency)
else -> detailsState
}
}
private fun handlePrepareScreen(
action: DetailsAction.PrepareScreen,
state: DetailsState,
): DetailsState {
return DetailsState(
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
appCurrency = store.state.globalState.appCurrency,
)
}
private fun handlePrepareCardSettingsScreen(card: Card, state: DetailsState): DetailsState {
val cardSettingsState = CardSettingsState(
cardInfo = card.toCardInfo(),
manageSecurityState = prepareSecurityOptions(card),
card = card,
resetCardAllowed = isResetToFactoryAllowedByCard(card),
)
return state.copy(cardSettingsState = cardSettingsState)
}
private fun prepareSecurityOptions(card: Card): ManageSecurityState {
val securityOption = when {
card.isAccessCodeSet -> {
SecurityOption.AccessCode
}
card.isPasscodeSet == true -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
val allowedSecurityOptions = when {
card.isStart2Coin || card.isTangemNote() -> {
EnumSet.of(SecurityOption.LongTap)
}
card.settings.isBackupAllowed -> {
EnumSet.of(securityOption)
}
else -> prepareAllowedSecurityOptions(card, securityOption)
}
return ManageSecurityState(
currentOption = securityOption,
allowedOptions = allowedSecurityOptions,
selectedOption = securityOption,
)
}
private fun isResetToFactoryAllowedByCard(card: Card): Boolean {
val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent }
val notAllowedByCard = notAllowedByAnyWallet ||
(card.isWalletDataSupported && (!card.isTangemNote() && !card.settings.isBackupAllowed)) ||
card.isSaltPay
return !notAllowedByCard
}
private fun handleEraseWallet(
action: DetailsAction.ResetToFactory,
state: DetailsState,
): DetailsState {
return when (action) {
DetailsAction.ResetToFactory.Check -> {
val card = state.scanResponse?.card
val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet ||
(card?.isWalletDataSupported == true &&
(!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup()))
val notEmpty = state.wallets.any { it.hasSendableAmountsOrPendingTransactions() }
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard
notEmpty -> EraseWalletState.NotEmpty
else -> EraseWalletState.Allowed
}
state.copy(eraseWalletState = eraseWalletState)
}
DetailsAction.ResetToFactory.Proceed -> {
if (state.eraseWalletState == EraseWalletState.Allowed) {
state.copy(confirmScreenState = ConfirmScreenState.EraseWallet)
} else {
state
}
}
DetailsAction.ResetToFactory.Cancel -> state.copy(eraseWalletState = null)
DetailsAction.ResetToFactory.Failure -> state.copy(eraseWalletState = null)
DetailsAction.ResetToFactory.Success -> state.copy(eraseWalletState = null)
is DetailsAction.ResetToFactory.Confirm ->
state.copy(cardSettingsState = state.cardSettingsState?.copy(resetConfirmed = action.confirmed))
else -> state
}
}
@ -90,68 +117,45 @@ private fun handleSecurityAction(
action: DetailsAction.ManageSecurity, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SetCurrentOption -> {
val securityOption = when {
action.userCodes.isAccessCodeSet -> {
SecurityOption.AccessCode
}
action.userCodes.isPasscodeSet -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
state.copy(securityScreenState = SecurityScreenState(currentOption = securityOption))
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
val allowedSecurityOptions = when {
state.scanResponse?.card?.isStart2Coin == true ||
state.scanResponse?.isTangemNote() == true -> {
EnumSet.of(SecurityOption.LongTap)
}
state.scanResponse?.supportsBackup() == true -> {
EnumSet.of(state.securityScreenState?.currentOption)
}
else -> prepareAllowedSecurityOptions(
state.scanResponse?.card, state.securityScreenState?.currentOption
)
}
state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = allowedSecurityOptions,
selectedOption = state.securityScreenState.currentOption
))
}
is DetailsAction.ManageSecurity.SelectOption -> {
state.copy(securityScreenState = state.securityScreenState?.copy(
selectedOption = action.option
))
}
is DetailsAction.ManageSecurity.ConfirmSelection -> {
val confirmScreenState = when (action.option) {
SecurityOption.LongTap -> ConfirmScreenState.LongTap
SecurityOption.PassCode -> ConfirmScreenState.PassCode
SecurityOption.AccessCode -> ConfirmScreenState.AccessCode
}
state.copy(confirmScreenState = confirmScreenState)
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
selectedOption = action.option,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState))
}
is DetailsAction.ManageSecurity.SaveChanges.Success -> {
// Setting options to show only LongTap from now on for non-twins
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
currentOption = state.cardSettingsState.manageSecurityState.selectedOption,
allowedOptions = state.scanResponse?.card?.let {
prepareAllowedSecurityOptions(
it, state.cardSettingsState.manageSecurityState.selectedOption,
)
} ?: EnumSet.of(SecurityOption.LongTap),
)
state.copy(
securityScreenState = state.securityScreenState?.copy(
currentOption = state.securityScreenState.selectedOption,
allowedOptions = state.scanResponse?.card?.let {
prepareAllowedSecurityOptions(
it, state.securityScreenState.selectedOption
)
} ?: EnumSet.of(SecurityOption.LongTap)
))
cardSettingsState = state.cardSettingsState?.copy(
manageSecurityState = manageSecurityState,
),
)
}
else -> state
}
}
private fun handlePrivacyAction(
action: DetailsAction.AppSettings, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
when (action.setting) {
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
}
}
}
}
private fun prepareAllowedSecurityOptions(
card: Card?, currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
@ -169,7 +173,6 @@ private fun prepareAllowedSecurityOptions(
return allowedSecurityOptions
}
private fun Card.toCardInfo(): CardInfo {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.issuer.name

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
@ -14,13 +15,13 @@ import kotlin.properties.ReadOnlyProperty
data class DetailsState(
val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val cardSettingsState: CardSettingsState? = null,
val cardTermsOfUseUrl: Uri? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default
val appCurrency: FiatCurrency = FiatCurrency.Default,
val saveWallets: Boolean = true,
val saveAccessCodes: Boolean = true,
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -39,13 +40,23 @@ data class CardInfo(
val signedHashes: Int,
)
enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty }
enum class ConfirmScreenState { EraseWallet, LongTap, AccessCode, PassCode }
data class SecurityScreenState(
data class CardSettingsState(
val cardInfo: CardInfo,
val card: Card,
val manageSecurityState: ManageSecurityState?,
val resetCardAllowed: Boolean,
val resetConfirmed: Boolean = false,
)
data class ManageSecurityState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true),
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class PrivacySetting {
SaveWallets, SaveAccessCode
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.details.redux.walletconnect
import android.app.Activity
import com.tangem.domain.common.ScanResponse
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.wallet.R
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
@ -18,25 +17,21 @@ sealed class WalletConnectAction : Action {
object RestoreSessions : WalletConnectAction()
data class StartWalletConnect(
val activity: Activity,
val copiedUri: String?,
) : WalletConnectAction()
data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction()
object UnsupportedCard : WalletConnectAction()
data class OpenSession(
val wcUri: String,
) : WalletConnectAction()
data class AddScanResponse(
val scanResponse: ScanResponse
): WalletConnectAction()
data class SetNewSessionData(
val newSession: NewWcSessionData,
) : WalletConnectAction()
object RefuseOpeningSession : WalletConnectAction()
data class OpeningSessionTimeout(val session: WCSession) : WalletConnectAction()
data class ScanCard(
val session: WalletConnectSession, val chainId: Int?,
) : WalletConnectAction()
@ -47,8 +42,18 @@ sealed class WalletConnectAction : Action {
data class Success(val session: WalletConnectSession) : WalletConnectAction()
}
data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction()
data class SwitchBlockchain(
val blockchain: Blockchain?,
val session: WalletConnectSession,
) : WalletConnectAction()
data class SelectNetwork(val session: WalletConnectSession, val networks: List<Blockchain>) : WalletConnectAction()
data class ChooseNetwork(val blockchain: Blockchain) : WalletConnectAction()
data class UpdateBlockchain(
val updatedSession: WalletConnectSession,
) : WalletConnectAction()
data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :
WalletConnectAction()

View file

@ -1,38 +1,35 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.redux.AppState
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.currenciesRepository
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.domain.walletconnect.WcWalletManagerFactory
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class WalletConnectMiddleware {
private val walletConnectManager = WalletConnectManager()
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
@ -57,20 +54,36 @@ class WalletConnectMiddleware {
}
}
is WalletConnectAction.StartWalletConnect -> {
val uri = action.activity.getFromClipboard()?.toString()
val uri = action.copiedUri
if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
}
}
is WalletConnectAction.SelectNetwork -> {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.ChooseNetwork(
session = action.session,
networks = action.networks,
),
),
)
}
is WalletConnectAction.ChooseNetwork -> {
val data = state()?.walletConnectState?.newSessionData ?: return
scope.launch {
prepareWalletManager(data.scanResponse, store.state.walletState, action.blockchain, data.session)
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ClipboardOrScanQr(
action.wcUri
)
)
action.wcUri,
),
),
)
}
is WalletConnectAction.OpeningSessionTimeout -> {
@ -92,8 +105,8 @@ class WalletConnectMiddleware {
is WalletConnectAction.RefuseOpeningSession -> {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.OpeningSessionRejected
)
WalletConnectDialog.OpeningSessionRejected,
),
)
}
is WalletConnectAction.ScanCard -> {
@ -110,14 +123,14 @@ class WalletConnectMiddleware {
transaction = action.transaction,
session = action.session,
id = action.id,
type = action.type
type = action.type,
)
}
is WalletConnectAction.HandlePersonalSignRequest -> {
walletConnectManager.handlePersonalSignRequest(
message = action.message,
session = action.session,
id = action.id
id = action.id,
)
}
is WalletConnectAction.RejectRequest -> {
@ -138,9 +151,9 @@ class WalletConnectMiddleware {
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
dAppName = action.sessionData.peerMeta.name,
),
),
)
}
is WalletConnectAction.BinanceTransaction.Transfer -> {
@ -152,126 +165,166 @@ class WalletConnectMiddleware {
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
dAppName = action.sessionData.peerMeta.name,
),
),
)
}
is WalletConnectAction.BinanceTransaction.Sign -> {
walletConnectManager.signBnb(
action.id, action.data, action.sessionData
action.id, action.data, action.sessionData,
)
}
is WalletConnectAction.SwitchBlockchain -> {
if (action.session.wallet.derivationStyle == DerivationStyle.LEGACY) {
Timber.d("Cannot switch chains on AC01/AC02 wallets")
return
}
val blockchain = action.blockchain.guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository,
)
val walletState = store.state.walletState
scope.launch {
val walletManager = factory.getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = walletState,
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchain.fullName),
),
)
return@launch
}
val updatedWallet = action.session.wallet.copy(
walletPublicKey = walletManager.wallet.publicKey.seedKey,
derivedPublicKey = walletManager.wallet.publicKey.derivedKey,
derivationPath = walletManager.wallet.publicKey.derivationPath,
blockchain = action.blockchain,
)
val updatedSession = action.session.copy(wallet = updatedWallet)
store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession))
}
}
is WalletConnectAction.UpdateBlockchain -> {
walletConnectManager.updateBlockchain(action.updatedSession)
}
}
}
private fun scanCard(session: WalletConnectSession, chainId: Int?) {
val blockchain = WalletConnectNetworkUtils.parseBlockchain(
chainId = chainId, peer = session.peerMeta
chainId = chainId,
peer = session.peerMeta,
) ?: Blockchain.Ethereum
store.dispatch(
GlobalAction.ScanCard(
additionalBlockchainsToDerive = listOf(blockchain),
onSuccess = { scanResponse ->
handleScanResponse(scanResponse, session, blockchain)
handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain)
},
onFailure = {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
}, R.string.wallet_connect_scan_card_message
)
},
R.string.wallet_connect_scan_card_message,
),
)
}
private suspend fun getAvailableBlockchains(card: Card, walletState: WalletState): List<Blockchain> {
return if (walletState.cardId == card.cardId) {
walletState.currencies.filter {
it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm()
}.map { it.blockchain }
} else {
currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.filter {
it.blockchain.isEvm() &&
it.derivationPath == it.blockchain.derivationPath(card.derivationStyle)?.rawPath
}
.map { it.blockchain }
}
}
private suspend fun prepareWalletManager(
scanResponse: ScanResponse,
walletState: WalletState,
blockchain: Blockchain,
session: WalletConnectSession,
) {
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository,
)
val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchain.fullName),
),
)
return
}
val wallet = walletManager.wallet
val derivedKey =
if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) {
null
} else {
walletManager.wallet.publicKey.blockchainKey
}
val walletForSession = WalletForSession(
cardId = scanResponse.card.cardId,
walletPublicKey = wallet.publicKey.seedKey,
derivedPublicKey = derivedKey,
derivationPath = wallet.publicKey.derivationPath,
derivationStyle = scanResponse.card.derivationStyle,
blockchain = wallet.blockchain,
)
withMainContext {
val updatedSession = session.copy(wallet = walletForSession)
walletConnectManager.updateSession(updatedSession)
store.dispatch(WalletConnectAction.ApproveSession(session.session))
}
}
private fun handleScanResponse(
scanResponse: ScanResponse, session: WalletConnectSession, blockchain: Blockchain
scanResponse: ScanResponse,
session: WalletConnectSession,
blockchain: Blockchain,
) {
val card = scanResponse.card
if (!card.isMultiwalletAllowed) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return
}
val walletManager = getWalletManager(scanResponse, blockchain).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
return
}
val wallet = walletManager.wallet
val derivedKey =
if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) {
null
} else {
walletManager.wallet.publicKey.blockchainKey
}
val walletForSession = WalletForSession(
cardId = scanResponse.card.cardId,
walletPublicKey = wallet.publicKey.seedKey,
derivedPublicKey = derivedKey,
derivationPath = wallet.publicKey.derivationPath,
blockchain = wallet.blockchain
val walletState = store.state.walletState
val updatedSession = session.copy(wallet = session.wallet.copy(blockchain = blockchain))
store.dispatch(
WalletConnectAction.SetNewSessionData(
NewWcSessionData(session = updatedSession, scanResponse = scanResponse, blockchain = blockchain),
),
)
val updatedSession = session.copy(wallet = walletForSession)
walletConnectManager.updateSession(updatedSession)
store.dispatchOnMain(WalletConnectAction.AddScanResponse(scanResponse))
scope.launch {
val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList()
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(
updatedSession
withMainContext {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains),
),
)
)
)
}
private fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain
): WalletManager? {
val card = scanResponse.card
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
val derivationPath = blockchainToMake.derivationPath(card.derivationStyle)?.rawPath
store.state.walletState.getWalletManager(
Currency.Blockchain(blockchainToMake, derivationPath)
)
?: factory.makeWalletManagerForApp(
scanResponse,
blockchainToMake,
card.derivationStyle?.let { DerivationParams.Default(it) }
)
?.also { walletManager ->
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
BlockchainNetwork.fromWalletManager(walletManager), walletManager
)
)
}
} else {
val walletManager = factory.makeWalletManagerForApp(
scanResponse,
blockchainToMake,
card.derivationStyle?.let { DerivationParams.Default(it) }
)
scope.launch {
if (currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.find { it.blockchain == blockchainToMake } != null
) {
walletManager?.let {
currenciesRepository.saveUpdatedCurrency(
card.cardId,
BlockchainNetwork.fromWalletManager(walletManager)
)
}
}
}
return walletManager
}
}
}

View file

@ -13,18 +13,17 @@ class WalletConnectReducer {
is WalletConnectAction.ApproveSession.Success -> {
state.copy(
loading = false,
sessions = state.sessions + action.session
sessions = state.sessions + action.session,
)
}
is WalletConnectAction.OpenSession -> {
is WalletConnectAction.OpenSession -> {
state.copy(loading = true)
}
is WalletConnectAction.AddScanResponse -> {
state.copy(scanResponse = action.scanResponse)
is WalletConnectAction.SetNewSessionData -> {
state.copy(newSessionData = action.newSession)
}
is WalletConnectAction.SetSessionsRestored ->
WalletConnectState(sessions = action.sessions)
is WalletConnectAction.RemoveSession -> {
val sessions =
state.sessions.filterNot { it.session.toUri() == action.session.toUri() }
@ -34,6 +33,11 @@ class WalletConnectReducer {
is WalletConnectAction.RefuseOpeningSession -> state.copy(loading = false)
is WalletConnectAction.OpeningSessionTimeout -> state.copy(loading = false)
is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false)
is WalletConnectAction.UpdateBlockchain -> state.copy(
sessions = state.sessions
.filterNot { it.peerId == action.updatedSession.peerId }
+ action.updatedSession
)
else -> state
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux.walletconnect
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.hdWallet.DerivationPath
@ -15,7 +16,13 @@ import com.trustwallet.walletconnect.models.session.WCSession
data class WalletConnectState(
val loading: Boolean = false,
val sessions: List<WalletConnectSession> = listOf(),
val scanResponse: ScanResponse? = null
val newSessionData: NewWcSessionData? = null,
)
data class NewWcSessionData(
val session: WalletConnectSession,
val scanResponse: ScanResponse,
val blockchain: Blockchain?,
)
data class WalletConnectSession(
@ -37,8 +44,9 @@ data class WalletForSession(
val walletPublicKey: ByteArray? = null,
val derivedPublicKey: ByteArray? = null,
val derivationPath: DerivationPath? = null,
val derivationStyle: DerivationStyle? = null,
val isTestNet: Boolean = false,
val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum
val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum,
) {
fun getBlockchainForSession(): Blockchain {
@ -82,18 +90,30 @@ data class WalletForSession(
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
object UnsupportedCard : WalletConnectDialog()
object UnsupportedNetwork : WalletConnectDialog()
data class AddNetwork(val network: String) : WalletConnectDialog()
object OpeningSessionRejected : WalletConnectDialog()
object SessionTimeout : WalletConnectDialog()
data class ApproveWcSession(val session: WalletConnectSession) : WalletConnectDialog()
data class ApproveWcSession(
val session: WalletConnectSession,
val networks: List<Blockchain>,
) : WalletConnectDialog()
data class ChooseNetwork(
val session: WalletConnectSession,
val networks: List<Blockchain>,
) : WalletConnectDialog()
data class RequestTransaction(val dialogData: TransactionRequestDialogData) :
WalletConnectDialog()
data class PersonalSign(val data: PersonalSignDialogData) : WalletConnectDialog()
data class BnbTransactionDialog(val data: BinanceMessageData,
val session: WCSession,
val sessionId: Long,
val cardId: String,
val dAppName: String
data class BnbTransactionDialog(
val data: BinanceMessageData,
val session: WCSession,
val sessionId: Long,
val cardId: String,
val dAppName: String,
) : WalletConnectDialog()
}

View file

@ -1,88 +0,0 @@
package com.tangem.tap.features.details.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.getDrawable
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.ConfirmScreenState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsConfirmBinding
import org.rekotlin.StoreSubscriber
class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm),
StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsConfirmBinding by viewBinding(FragmentDetailsConfirmBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
setState(state)
}
private fun setState(state: DetailsState) = with(binding) {
when (state.confirmScreenState) {
ConfirmScreenState.EraseWallet -> {
toolbar.title = getString(R.string.details_row_title_reset_factory_settings)
tvWarningDescription.text = getString(R.string.details_row_title_reset_factory_settings_warning)
btnConfirm.text = getString(R.string.details_row_title_reset_factory_settings)
btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_send), null
)
btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) }
}
ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode,
ConfirmScreenState.PassCode -> {
toolbar.title = getString(R.string.details_manage_security_title)
tvWarningDescription.text = getString(R.string.details_security_management_warning)
btnConfirm.text = getString(R.string.common_save_changes)
btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_save), null
)
btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) }
}
}
}
}

View file

@ -1,148 +0,0 @@
package com.tangem.tap.features.details.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsBinding
import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
setState(state)
}
private fun setState(state: DetailsState) = with (binding){
if (state.cardInfo != null) {
val cardId = if (state.isTangemTwins) {
state.scanResponse?.card?.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
tvCardId.text = cardId
tvIssuer.text = state.cardInfo.issuer
tvSignedHashes.text = getString(
R.string.details_row_subtitle_signed_hashes_format,
state.cardInfo.signedHashes.toString()
)
}
tvSignedHashes.show(!state.isTangemTwins)
tvSignedHashesTitle.show(!state.isTangemTwins)
tvDisclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tvCardTou.show(state.cardTermsOfUseUrl != null)
tvCardTou.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = state.cardTermsOfUseUrl
startActivity(intent)
}
if (state.isTangemTwins) {
tvResetToFactory.show()
tvResetToFactory.text = getText(R.string.details_row_title_twins_recreate)
tvResetToFactory.setOnClickListener {
store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!))
}
} else {
tvResetToFactory.show()
tvResetToFactory.text = getText(R.string.details_row_title_reset_factory_settings)
tvResetToFactory.setOnClickListener {
store.dispatch(DetailsAction.ResetToFactory.Check)
store.dispatch(DetailsAction.ResetToFactory.Proceed)
}
}
tvCreateBackup.show(state.createBackupAllowed)
tvCreateBackup.setOnClickListener {
store.dispatch(DetailsAction.CreateBackup)
}
tvAppCurrency.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
tvAppCurrencyTitle.show(state.scanResponse?.card?.isMultiwalletAllowed != true)
tvAppCurrency.text = state.appCurrency.code
tvAppCurrency.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
tvSendFeedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
tvWalletConnect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
tvWalletConnect.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
llManageSecurity.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
}
val currentSecurity = when (state.securityScreenState?.currentOption) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
SecurityOption.PassCode -> R.string.details_manage_security_passcode
SecurityOption.AccessCode -> R.string.details_manage_security_access_code
null -> null
}
currentSecurity?.let { tvSecurity.text = getString(it) }
}
}

View file

@ -1,147 +0,0 @@
package com.tangem.tap.features.details.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDetailsSecurityBinding
import org.rekotlin.StoreSubscriber
import java.util.*
class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security),
StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsSecurityBinding by viewBinding(
FragmentDetailsSecurityBinding::bind
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
setOnClickListeners()
}
private fun setOnClickListeners() {
binding.btnSaveChanges.setOnClickListener {
store.state.detailsState.securityScreenState?.selectedOption?.let {
store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection(it))
}
}
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
selectSecurityOption(state.securityScreenState?.selectedOption)
for (option in SecurityOption.values()) {
setupOption(
option,
state.securityScreenState?.allowedOptions
?: EnumSet.noneOf(SecurityOption::class.java)
)
}
binding.tvAccessCodeUnavailableDisclaimer.show(
state.scanResponse?.card?.backupStatus == Card.BackupStatus.NoBackup
)
}
private fun selectSecurityOption(securityOption: SecurityOption?) = with(binding) {
radiobuttonLongTap.isChecked =
securityOption == SecurityOption.LongTap && radiobuttonLongTap.isEnabled
radiobuttonPasscode.isChecked =
securityOption == SecurityOption.PassCode && radiobuttonPasscode.isEnabled
radiobuttonAccessCode.isChecked =
securityOption == SecurityOption.AccessCode && radiobuttonAccessCode.isEnabled
}
private fun setupOption(option: SecurityOption, allowedOptions: EnumSet<SecurityOption>) {
when (option) {
SecurityOption.LongTap -> { enableLongTap(allowedOptions.contains(option)) }
SecurityOption.PassCode -> { enablePasscode(allowedOptions.contains(option)) }
SecurityOption.AccessCode -> { enableAccessCode(allowedOptions.contains(option)) }
}
}
private fun enableLongTap(enable: Boolean) = with(binding) {
groupLongTap.show(enable)
val alpha = if (enable) 1f else 0.5f
tvLongTapDescription.alpha = alpha
tvLongTapTitle.alpha = alpha
radiobuttonLongTap.alpha = alpha
radiobuttonLongTap.isEnabled = enable
vLongTap.isClickable = enable
if (enable) {
vLongTap.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.LongTap))
}
}
}
private fun enablePasscode(enable: Boolean) = with(binding) {
groupPasscode.show(enable)
val alpha = if (enable) 1f else 0.5f
tvPasscodeDescription.alpha = alpha
tvPasscodeTitle.alpha = alpha
radiobuttonPasscode.alpha = alpha
radiobuttonPasscode.isEnabled = enable
vPasscode.isClickable = enable
if (enable) {
vPasscode.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.PassCode))
}
}
}
private fun enableAccessCode(enable: Boolean) = with(binding) {
groupAccessCode.show(enable)
val alpha = if (enable) 1f else 0.5f
tvAccessCodeDescription.alpha = alpha
tvAccessCodeTitle.alpha = alpha
radiobuttonAccessCode.alpha = alpha
radiobuttonAccessCode.isEnabled = enable
vAccessCode.isClickable = enable
if (enable) {
vAccessCode.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.AccessCode))
}
}
}
}

View file

@ -0,0 +1,71 @@
package com.tangem.tap.features.details.ui.appsettings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = AppSettingsViewModel(store)
private var screenState: MutableState<AppSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
AppSettingsScreen(
state = screenState.value,
onBackPressed = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state)
}
}

View file

@ -0,0 +1,230 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.AlertDialog
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.tap.features.details.ui.common.TangemSwitch
import com.tangem.wallet.R
@Composable
fun AppSettingsScreen(
state: AppSettingsScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = {
AppSettings(state = state, modifier = modifier)
},
titleRes = R.string.app_settings_title,
onBackClick = onBackPressed,
)
}
@Composable
private fun AppSettings(
state: AppSettingsScreenState,
modifier: Modifier = Modifier,
) {
var dialogType by remember { mutableStateOf<PrivacySetting?>(null) }
val onDialogStateChange: (PrivacySetting?) -> Unit = { dialogType = it }
dialogType?.let {
SettingsAlertDialog(
element = it,
onDialogStateChange = onDialogStateChange,
onSettingToggled = state.onSettingToggled,
modifier = modifier,
)
}
Column(
modifier = modifier
.fillMaxSize(),
) {
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveWallets,
onDialogStateChange = onDialogStateChange,
modifier = modifier,
)
Spacer(modifier = Modifier.size(32.dp))
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveAccessCode,
onDialogStateChange = onDialogStateChange,
modifier = modifier,
)
}
}
@Composable
private fun AppSettingsElement(
state: AppSettingsScreenState,
setting: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
modifier: Modifier = Modifier,
) {
val titleRes = when (setting) {
PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet
PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes
}
val subtitleRes = when (setting) {
PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet_footer
PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer
}
val checked = state.settings[setting] ?: false
Row(
modifier = modifier
.fillMaxWidth()
.padding(start = 20.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier
.weight(0.6f)
.padding(end = 4.dp),
) {
Text(
text = stringResource(id = titleRes),
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = Modifier.size(4.dp))
Text(
text = stringResource(id = subtitleRes),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
TangemSwitch(
checked = checked,
onCheckedChange = {
onCheckedChange(
element = setting,
enabled = it,
onSettingToggled = state.onSettingToggled,
onDialogStateChange = onDialogStateChange,
)
},
modifier = modifier
.padding(20.dp),
)
}
}
private fun onCheckedChange(
element: PrivacySetting, enabled: Boolean,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
onDialogStateChange: (PrivacySetting?) -> Unit,
) {
//Show warning if user wants to disable the switch
if (!enabled) {
onDialogStateChange(element)
} else {
onSettingToggled(element, enabled)
}
}
@Composable
private fun SettingsAlertDialog(
element: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val text = when (element) {
PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
}
AlertDialog(
onDismissRequest = { onDialogStateChange(null) },
confirmButton = {
TextButton(
onClick = {
onDialogStateChange(null)
},
) {
Text(
text = stringResource(id = R.string.common_cancel),
color = colorResource(id = R.color.text_secondary),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
},
dismissButton = {
TextButton(
onClick = {
onDialogStateChange(null)
onSettingToggled(element, false)
},
modifier = modifier.padding(bottom = 14.dp),
) {
Text(
text = stringResource(id = R.string.common_delete),
color = colorResource(id = R.color.text_warning),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
},
title = {
Text(
text = stringResource(id = R.string.common_attention),
color = colorResource(id = R.color.text_primary_1),
)
},
text = {
Text(
text = stringResource(id = text),
color = colorResource(id = R.color.text_secondary),
)
},
shape = MaterialTheme.shapes.medium.copy(all = CornerSize(size = 28.dp)),
modifier = modifier.padding(vertical = 34.dp, horizontal = 24.dp),
)
}
@Preview
@Composable
fun AppSettingsScreenPreview() {
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to true,
PrivacySetting.SaveAccessCode to false,
),
onSettingToggled = { _, _ -> },
),
onBackPressed = { },
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.tap.features.details.redux.PrivacySetting
data class AppSettingsScreenState(
val settings: Map<PrivacySetting, Boolean>,
val onSettingToggled: (PrivacySetting, Boolean) -> Unit,
)

View file

@ -0,0 +1,27 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.PrivacySetting
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.rekotlin.Store
class AppSettingsViewModel(private val store: Store<AppState>) {
private val _uiState = MutableStateFlow(updateState(store.state.detailsState))
val uiState: StateFlow<AppSettingsScreenState> = _uiState
fun updateState(state: DetailsState): AppSettingsScreenState {
return AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to state.saveWallets,
PrivacySetting.SaveAccessCode to state.saveAccessCodes,
),
onSettingToggled = { privacySetting, enabled -> onSettingsToggled(privacySetting, enabled) },
)
}
private fun onSettingsToggled(setting: PrivacySetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
}
}

View file

@ -0,0 +1,73 @@
package com.tangem.tap.features.details.ui.cardsettings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
class CardSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = CardSettingsViewModel(store)
private var screenState: MutableState<CardSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
CardSettingsScreen(
state = screenState.value,
onBackPressed = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState)
}
}

View file

@ -0,0 +1,169 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun CardSettingsScreen(
state: CardSettingsScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content =
if (state.cardDetails == null) {
{ CardSettingsReadCard(state.onScanCardClick, modifier = modifier) }
} else {
{ CardSettings(state = state, modifier = modifier) }
},
titleRes = R.string.card_settings_title,
onBackClick = onBackPressed,
)
}
@Composable
fun CardSettingsReadCard(
onScanCardClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Box(
modifier = modifier
.fillMaxWidth(),
) {
Image(
modifier = modifier
.fillMaxWidth()
.padding(start = 80.dp, end = 80.dp, top = 70.dp)
.rotate(-15f),
painter = painterResource(id = R.drawable.card_placeholder_secondary),
contentDescription = "",
contentScale = ContentScale.FillWidth,
)
Image(
modifier = modifier
.fillMaxWidth()
.padding(start = 60.dp, end = 60.dp)
.rotate(-1f),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = "",
contentScale = ContentScale.FillWidth,
)
}
Column(
modifier = modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
Text(
text = stringResource(id = R.string.scan_card_settings_title),
color = colorResource(id = R.color.text_primary_1),
style = TangemTypography.headline3,
)
Spacer(modifier = modifier.size(20.dp))
Text(
text = stringResource(id = R.string.scan_card_settings_message),
color = colorResource(id = R.color.text_secondary),
style = TangemTypography.body1,
)
Spacer(modifier = modifier.size(29.dp))
DetailsMainButton(
title = stringResource(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
modifier = modifier,
)
}
}
}
@Composable
fun CardSettings(
state: CardSettingsScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth(),
) {
state.cardDetails?.map {
val paddingBottom = when (it) {
is CardInfo.CardId, is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 14.dp
is CardInfo.SecurityMode -> 16.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.ResetToFactorySettings -> 28.dp
}
val paddingTop = when (it) {
is CardInfo.CardId -> 0.dp
is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 12.dp
is CardInfo.SecurityMode -> 14.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.ResetToFactorySettings -> 16.dp
}
Column(
modifier = modifier
.fillMaxWidth()
.clickable(
enabled = it.clickable,
onClick = { state.onElementClick(it) },
)
.padding(start = 20.dp, end = 20.dp, bottom = paddingBottom, top = paddingTop),
) {
val titleColor = if (it.clickable) R.color.text_primary_1 else R.color.text_tertiary
val subtitleColor = if (it.clickable) R.color.text_secondary else R.color.text_tertiary
Text(
text = it.titleRes.resolveReference(),
color = colorResource(id = titleColor),
style = TangemTypography.subtitle1,
)
Spacer(modifier = modifier.size(4.dp))
Text(
text = it.subtitle.resolveReference(),
color = colorResource(id = subtitleColor),
style = TangemTypography.body2,
)
}
}
}
}
@Composable
@Preview
fun CardSettingsPreview() {
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {})
}

View file

@ -0,0 +1,74 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.wallet.R
data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
)
sealed class CardInfo(
val titleRes: TextReference, val subtitle: TextReference, val clickable: Boolean = false,
) {
class CardId(subtitle: String) : CardInfo(
titleRes = TextReference.Res(R.string.details_row_title_cid),
subtitle = TextReference
.Str(subtitle),
)
class Issuer(subtitle: String) : CardInfo(
titleRes = TextReference.Res(R.string.details_row_title_issuer),
subtitle = TextReference
.Str(subtitle),
)
class SignedHashes(hashes: String) : CardInfo(
titleRes = TextReference.Res(R.string.details_row_title_signed_hashes),
subtitle = TextReference.Res(R.string.details_row_subtitle_signed_hashes_format, hashes),
)
class SecurityMode(
securityOption: SecurityOption,
clickable: Boolean,
) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_security_mode),
subtitle = TextReference.Res(securityOption.toTitleRes()),
clickable = clickable,
)
object ChangeAccessCode : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_change_access_code),
subtitle = TextReference.Res(R.string.card_settings_change_access_code_footer),
clickable = true,
)
object ResetToFactorySettings : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
subtitle = TextReference.Res(R.string.card_settings_reset_card_to_factory_footer),
clickable = true,
)
}
sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
}
class Str(val value: String) : TextReference
}
@Composable
@ReadOnlyComposable
fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value
}
}

View file

@ -0,0 +1,73 @@
package com.tangem.tap.features.details.ui.cardsettings
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.common.isTangemTwins
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
class CardSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) {
CardSettingsScreenState(
cardDetails = null,
onElementClick = {},
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
},
)
} else {
val cardId = if (state.card.isTangemTwins()) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
val cardDetails: MutableList<CardInfo> = mutableListOf(
CardInfo.CardId(cardId),
CardInfo.Issuer(state.cardInfo.issuer),
)
if (!state.card.isTangemTwins()) {
cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
}
cardDetails.add(
CardInfo.SecurityMode(
securityOption = state.manageSecurityState.currentOption,
clickable = state.manageSecurityState.allowedOptions.size > 1,
),
)
if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
cardDetails.add(CardInfo.ChangeAccessCode)
}
if (state.resetCardAllowed) {
cardDetails.add(CardInfo.ResetToFactorySettings)
}
CardSettingsScreenState(
cardDetails = cardDetails,
onScanCardClick = { },
onElementClick = {
handleClickingItem(it)
},
)
}
}
private fun handleClickingItem(item: CardInfo) {
when (item) {
is CardInfo.ChangeAccessCode -> {
store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
}
is CardInfo.ResetToFactorySettings -> {
store.dispatch(DetailsAction.ResetToFactory.Start)
}
is CardInfo.SecurityMode -> {
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
}
else -> {}
}
}
}

View file

@ -0,0 +1,134 @@
package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
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.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.wallet.R
@Composable
fun SettingsScreensScaffold(
content: @Composable () -> Unit,
background: @Composable (() -> Unit)? = null,
fab: @Composable (() -> Unit)? = null,
backgroundColor: Color = colorResource(id = R.color.background_primary),
titleRes: Int? = null,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(true, onBackClick)
Scaffold(
topBar = {
EmptyTopBarWithNavigation(
onBackClick = onBackClick,
backgroundColor = backgroundColor,
)
},
modifier = modifier.systemBarsPadding(),
backgroundColor = backgroundColor,
floatingActionButton = { fab?.invoke() },
) {
if (titleRes != null) {
Box(modifier = modifier.fillMaxSize()) {
background?.invoke()
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 52.dp),
style = TangemTypography.headline1,
color = colorResource(id = R.color.text_primary_1),
)
content()
}
}
} else {
content()
}
}
}
@Composable
fun ScreenTitle(
titleRes: Int,
modifier: Modifier = Modifier,
) {
Text(
text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
style = TangemTypography.headline1,
color = colorResource(id = R.color.text_primary_1),
)
}
@Composable
fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit,
backgroundColor: Color = colorResource(id = R.color.background_primary),
modifier: Modifier = Modifier,
) {
TopAppBar(
title = { },
navigationIcon =
{
IconButton(onClick = onBackClick) {
Icon(
painterResource(id = R.drawable.ic_back), "",
tint = colorResource(id = R.color.icon_primary_1),
)
}
},
backgroundColor = backgroundColor,
elevation = 0.dp,
)
}
@Composable
fun DetailsMainButton(
title: String,
enabled: Boolean = true,
onClick: (() -> Unit),
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.height(48.dp),
shape = RoundedCornerShape(12.dp),
enabled = enabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = colorResource(R.color.button_primary),
contentColor = colorResource(R.color.text_primary_2),
disabledBackgroundColor = colorResource(R.color.button_disabled),
disabledContentColor = colorResource(R.color.text_disabled),
),
) {
Text(text = title)
Spacer(modifier = modifier.size(8.dp))
Icon(painter = painterResource(id = R.drawable.ic_tangem), contentDescription = "")
}
}

View file

@ -0,0 +1,96 @@
package com.tangem.tap.features.details.ui.common
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.wallet.R
@Composable
fun TangemSwitch(
modifier: Modifier = Modifier,
enabledColor: Color = colorResource(id = R.color.control_checked),
disabledColor: Color = colorResource(id = R.color.icon_informative),
size: Dp = 48.dp,
checked: Boolean = false,
onCheckedChange: (Boolean) -> Unit,
) {
val transition = updateTransition(checked, label = "SwitchState")
val color by transition.animateColor(
transitionSpec = {
tween(durationMillis = 200, easing = FastOutLinearInEasing)
},
label = "",
) { enabled ->
if (enabled) enabledColor else disabledColor
}
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = modifier
.clickable(
interactionSource = interactionSource,
indication = null,
) {
onCheckedChange(!checked)
}
.indication(
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
color = Color.Transparent,
),
),
) {
BoxWithConstraints(
modifier = modifier
.width(size)
.height(size / 2)
.indication(interactionSource, null)
.background(color = color, shape = RoundedCornerShape(100)),
contentAlignment = Alignment.CenterStart,
) {
val roundCardSize = this.maxWidth / 2
val xOffset by transition.animateDp(
transitionSpec = {
tween(durationMillis = 150, easing = LinearOutSlowInEasing)
},
label = "xOffset",
) { enabled ->
if (enabled) this.maxWidth - roundCardSize else 0.dp
}
Box(
modifier = Modifier
.size(this.maxWidth / 2)
.offset(x = xOffset, y = 0.dp)
.padding(3.dp)
.background(color = Color.White, shape = RoundedCornerShape(100)),
)
}
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.tap.features.details.ui.details
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val detailsViewModel = DetailsViewModel(store)
private var detailsScreenState: MutableState<DetailsScreenState> =
mutableStateOf(detailsViewModel.updateState(store.state.detailsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
DetailsScreen(
state = detailsScreenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
detailsScreenState.value = detailsViewModel.updateState(state)
}
}

View file

@ -0,0 +1,197 @@
package com.tangem.tap.features.details.ui.details
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun DetailsScreen(
state: DetailsScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = { Content(state = state, modifier = modifier) },
onBackClick = onBackPressed,
)
}
@Composable
fun Content(
state: DetailsScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
ScreenTitle(titleRes = R.string.details_title, modifier.padding(bottom = 52.dp))
state.elements.map {
if (it == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(
onItemsClick = state.onItemsClick,
modifier = modifier,
)
} else {
DetailsItem(
item = it,
appCurrency = state.appCurrency,
onItemsClick = state.onItemsClick,
modifier = modifier,
)
}
}
Spacer(modifier = modifier.weight(1f))
TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick)
Spacer(modifier = Modifier.size(12.dp))
Text(
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
style = TangemTypography.caption,
color = colorResource(id = R.color.text_tertiary),
modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
)
}
}
@Composable
fun WalletConnectDetailsItem(
onItemsClick: (SettingsElement) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(84.dp)
.fillMaxWidth()
.clickable { onItemsClick(SettingsElement.WalletConnect) },
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = R.drawable.ic_walletconnect),
contentDescription = stringResource(id = R.string.wallet_connect_title),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.all_colors_azure),
)
Column(
modifier = modifier.height(56.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResource(id = R.string.wallet_connect_title),
modifier = modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTypography.headline3,
color = colorResource(id = R.color.text_primary_1),
)
Text(
text = stringResource(id = R.string.wallet_connect_subtitle),
modifier = modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTypography.body1,
color = colorResource(id = R.color.text_secondary),
)
}
}
}
@Composable
fun DetailsItem(
item: SettingsElement,
appCurrency: String,
onItemsClick: (SettingsElement) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.height(56.dp)
.fillMaxWidth()
.clickable { onItemsClick(item) },
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = item.iconRes),
contentDescription = stringResource(id = item.titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.icon_secondary),
)
Column(modifier = modifier.padding(end = 20.dp)) {
Text(
text = stringResource(id = item.titleRes),
modifier = modifier,
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
if (item == SettingsElement.AppCurrency) {
Text(
text = appCurrency,
modifier = modifier,
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
}
}
}
@Composable
fun TangemSocialAccounts(
links: List<TangemLink>,
onSocialNetworkClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
LazyRow(
modifier = modifier.padding(start = 8.dp, end = 8.dp),
) {
items(links) {
Icon(
painter = painterResource(id = it.iconRes),
contentDescription = "",
modifier = modifier
.padding(8.dp)
.clickable { onSocialNetworkClick(it.url) },
tint = colorResource(id = R.color.icon_informative),
)
}
}
}
@Composable
@Preview
fun Preview() {
DetailsScreen(
state = DetailsScreenState(
elements = SettingsElement.values().toList(),
tangemLinks = TangemSocialAccounts.accountsEn,
tangemVersion = "Tangem 2.14.12 (343)",
appCurrency = "Dollar",
onItemsClick = {}, onSocialNetworkClick = {},
),
onBackPressed = {},
)
}

View file

@ -0,0 +1,57 @@
package com.tangem.tap.features.details.ui.details
import com.tangem.wallet.R
data class DetailsScreenState(
val elements: List<SettingsElement>,
val tangemLinks: List<TangemLink>,
val tangemVersion: String,
val appCurrency: String,
val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (String) -> Unit,
) {
val appNameRes: Int = R.string.app_name
}
enum class SettingsElement(
val iconRes: Int,
val titleRes: Int,
) {
WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title),
Chat(R.drawable.ic_chat, R.string.details_ask_a_question),
SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title),
AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency),
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
TermsOfUse(R.drawable.ic_text, R.string.details_row_title_card_tou), // Terms of Use for S2C cards only
PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy),
;
}
data class TangemLink(
val iconRes: Int,
val url: String,
)
object TangemSocialAccounts {
val accountsEn: List<TangemLink> = listOf(
TangemLink(R.drawable.ic_telegram, "https://t.me/TangemCards"),
TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"),
TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"),
TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"),
TangemLink(R.drawable.ic_github, "https://github.com/tangem"),
TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"),
)
val accountsRu: List<TangemLink> = listOf(
TangemLink(R.drawable.ic_telegram, "https://t.me/tangem_ru"),
TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"),
TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"),
TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"),
TangemLink(R.drawable.ic_github, "https://github.com/tangem"),
TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"),
)
}

View file

@ -0,0 +1,104 @@
package com.tangem.tap.features.details.ui.details
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.wallet.BuildConfig
import org.rekotlin.Store
class DetailsViewModel(private val store: Store<AppState>) {
fun updateState(state: DetailsState): DetailsScreenState {
val settings = SettingsElement.values().mapNotNull {
when (it) {
SettingsElement.WalletConnect -> {
if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null
}
SettingsElement.LinkMoreCards -> {
if (state.createBackupAllowed) it else null
}
SettingsElement.PrivacyPolicy -> {
if (state.privacyPolicyUrl != null) it else null
}
SettingsElement.AppSettings -> null // TODO: until we implement settings from this screen
SettingsElement.AppCurrency -> if (state.scanResponse?.card?.isMultiwalletAllowed != true) it else null
SettingsElement.TermsOfUse -> if (state.scanResponse?.card?.isStart2Coin == true) it else null
else -> it
}
}
return DetailsScreenState(
settings,
tangemLinks = getSocialLinks(),
tangemVersion = getTangemAppVersion(),
appCurrency = state.appCurrency.name,
onItemsClick = { handleClickingSettingsItem(it) },
onSocialNetworkClick = { handleSocialNetworkClick(it) },
)
}
private fun handleSocialNetworkClick(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
private fun handleClickingSettingsItem(item: SettingsElement) {
when (item) {
SettingsElement.WalletConnect -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
SettingsElement.Chat -> {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
SettingsElement.SendFeedback -> {
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
}
SettingsElement.CardSettings -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
}
SettingsElement.AppCurrency -> {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
SettingsElement.AppSettings -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) //TODO: To be available later
}
SettingsElement.LinkMoreCards -> {
store.dispatch(DetailsAction.CreateBackup)
}
SettingsElement.TermsOfService -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
SettingsElement.TermsOfUse -> {
store.dispatch(DetailsAction.ShowDisclaimer)
}
SettingsElement.PrivacyPolicy -> {
// TODO: To be available later
}
}
}
private fun getSocialLinks(): List<TangemLink> {
val locale = LocaleRegionProvider().getRegion()
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
TangemSocialAccounts.accountsRu
} else {
TangemSocialAccounts.accountsEn
}
}
private fun getTangemAppVersion(): String {
val versionCode: Int = BuildConfig.VERSION_CODE
val versionName: String = BuildConfig.VERSION_NAME
return "$versionName ($versionCode)"
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.tap.features.details.ui.resetcard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
class ResetCardFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = ResetCardViewModel(store)
private var screenState: MutableState<ResetCardScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
ResetCardScreen(
state = screenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState)
}
}

View file

@ -0,0 +1,144 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconToggleButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun ResetCardScreen(
state: ResetCardScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.background(colorResource(id = R.color.background_primary))) {
Image(
painter = painterResource(id = R.drawable.ic_reset_background),
contentDescription = "",
modifier = modifier.offset(y = (-16).dp),
)
SettingsScreensScaffold(
content = { ResetCardView(state = state, modifier = modifier) },
onBackClick = onBackPressed,
backgroundColor = Color.Transparent,
)
}
}
@Composable
fun ResetCardView(
state: ResetCardScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
) {
Box(
modifier = modifier,
) {
ScreenTitle(titleRes = R.string.reset_card_to_factory_navigation_title)
}
Spacer(
modifier = modifier
.defaultMinSize(20.dp)
.weight(1f),
)
Text(
text = stringResource(id = R.string.common_attention),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
style = TangemTypography.headline3,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = modifier.size(24.dp))
Text(
text = stringResource(id = R.string.reset_card_to_factory_message),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
style = TangemTypography.body1,
color = colorResource(id = R.color.text_secondary),
)
Spacer(modifier = modifier.size(44.dp))
Row(
modifier = modifier
.fillMaxWidth()
.padding(end = 20.dp),
) {
IconToggleButton(
checked = state.accepted,
onCheckedChange = state.onAcceptWarningToggleClick,
modifier = modifier.padding(start = 20.dp, end = 20.dp),
) {
Icon(
painter = painterResource(
if (state.accepted) {
R.drawable.ic_accepted
} else {
R.drawable.ic_unticked
},
),
contentDescription = null,
tint = if (state.accepted) {
colorResource(id = R.color.icon_accent)
} else {
colorResource(id = R.color.icon_secondary)
},
)
}
Text(
text = stringResource(id = R.string.reset_card_to_factory_warning_message),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
Spacer(modifier = modifier.size(32.dp))
Box(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
DetailsMainButton(
title = stringResource(id = R.string.reset_card_to_factory_button_title),
onClick = state.onResetButtonClick,
enabled = state.resetButtonEnabled,
)
}
}
}
@Composable
@Preview
fun ResetCardScreenPreview(
) {
ResetCardScreen(state = ResetCardScreenState(onAcceptWarningToggleClick = {}, accepted = true) {}, {})
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.details.ui.resetcard
data class ResetCardScreenState(
val accepted: Boolean = false,
val onAcceptWarningToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
) {
val resetButtonEnabled: Boolean
get() = accepted
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.details.ui.resetcard
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
class ResetCardViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): ResetCardScreenState {
return ResetCardScreenState(
accepted = state?.resetConfirmed ?: false,
onAcceptWarningToggleClick = { store.dispatch(DetailsAction.ResetToFactory.Confirm(it)) },
onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) },
)
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.tap.features.details.ui.securitymode
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
class SecurityModeFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = SecurityModeViewModel(store)
private var screenState: MutableState<SecurityModeScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
SecurityModeScreen(
state = screenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState?.manageSecurityState)
}
}

View file

@ -0,0 +1,132 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun SecurityModeScreen(
state: SecurityModeScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = { SecurityModeOptions(state = state, modifier = modifier) },
titleRes = R.string.card_settings_security_mode,
onBackClick = onBackPressed,
)
}
@Composable
fun SecurityModeOptions(
state: SecurityModeScreenState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(bottom = 28.dp)
.offset(y = (-16).dp),
verticalArrangement = Arrangement.SpaceBetween,
) {
state.availableOptions.map {
SecurityOption(option = it, state = state, modifier = modifier)
}
Spacer(modifier = Modifier.weight(1f))
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = state.onSaveChangesClicked,
modifier = modifier
.padding(start = 20.dp, end = 20.dp),
)
}
}
@Composable
fun SecurityOption(
option: SecurityOption, state: SecurityModeScreenState,
modifier: Modifier,
) {
val selected = option == state.selectedSecurityMode
val title = option.toTitleRes()
val subtitle = when (option) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap_description
SecurityOption.PassCode -> R.string.details_manage_security_passcode_description
SecurityOption.AccessCode -> R.string.details_manage_security_access_code_description
}
Row(
modifier = modifier
.fillMaxWidth()
.selectable(
selected = selected, onClick = { state.onNewModeSelected(option) },
)
.padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
) {
RadioButton(
selected = selected, onClick = null,
modifier = modifier.padding(end = 20.dp),
colors = RadioButtonDefaults.colors(
unselectedColor = colorResource(id = R.color.icon_secondary),
selectedColor = colorResource(id = R.color.icon_accent),
),
)
Column {
Text(
text = stringResource(id = title),
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = modifier.size(4.dp))
Text(
text = stringResource(id = subtitle),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
}
}
@Preview
@Composable
fun SecurityModeScreenPreview() {
SecurityModeScreen(
state = SecurityModeScreenState(
availableOptions = SecurityOption.values().toList(),
selectedSecurityMode = SecurityOption.LongTap,
isSaveChangesEnabled = false,
onNewModeSelected = {},
onSaveChangesClicked = {},
),
onBackPressed = {},
)
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.details.ui.securitymode
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.wallet.R
data class SecurityModeScreenState(
val availableOptions: List<SecurityOption>,
val selectedSecurityMode: SecurityOption,
val isSaveChangesEnabled: Boolean,
val onNewModeSelected: (SecurityOption) -> Unit,
val onSaveChangesClicked: () -> Unit,
)
fun SecurityOption.toTitleRes(): Int {
return when (this) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
SecurityOption.PassCode -> R.string.details_manage_security_passcode
SecurityOption.AccessCode -> R.string.details_manage_security_access_code
}
}

Some files were not shown because too many files have changed in this diff Show more