Updated on 2026-08-14

This commit is contained in:
Tangem 2022-07-27 13:55:43 +04:00
commit 416205aa56
56 changed files with 1080 additions and 395 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
[*.{kt,kts}]
ktlint_code_style = official
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="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<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,12 +83,11 @@ 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'
@ -164,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"

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
@ -27,11 +29,11 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
@ -83,8 +85,10 @@ 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)
WindowInsetsControllerCompat(window, binding.root)
.isAppearanceLightStatusBars = true
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}

View file

@ -107,6 +107,12 @@ class DialogManager : StoreSubscriber<GlobalState> {
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)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)

View file

@ -63,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

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

@ -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,10 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
if (isDemoWallet() || TestActions.testAmountInjectionForWalletManagerEnabled) {
val scanResponse = store.state.globalState.scanResponse
?: error("Scan response must not be null")
if (scanResponse.isDemoCard() || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)

View file

@ -2,6 +2,7 @@ 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 {
@ -17,9 +18,9 @@ interface FeedbackData {
fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
appendLine(3)
breakLine(3)
append(context.getString(getDataCollectionMessageResId()))
appendLine()
breakLine()
append(createOptionalMessage(infoHolder))
}.toString()
}
@ -31,7 +32,7 @@ class RateCanBeBetterEmail : FeedbackData {
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendLine()
.breakLine()
.appendPhoneInfo()
.build()
}
@ -43,7 +44,7 @@ class ScanFailsEmail : FeedbackData {
override fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String = StringBuilder().apply {
append(context.getString(mainMessageResId))
appendLine(4)
breakLine(4)
append(createOptionalMessage(infoHolder))
}.toString()
@ -53,7 +54,7 @@ class ScanFailsEmail : FeedbackData {
}
class SendTransactionFailedEmail(
val error: String
val error: String,
) : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_tx_failed
@ -63,7 +64,7 @@ class SendTransactionFailedEmail(
.appendCardInfo()
.appendDelimiter()
.appendTxFailedBlockchainInfo(error)
.appendLine()
.breakLine()
.appendPhoneInfo()
.build()
}
@ -89,7 +90,7 @@ class FeedbackEmail : FeedbackData {
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendWalletsInfo()
.appendLine()
.breakLine()
.appendPhoneInfo()
.build()
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.feedback
import com.tangem.tap.common.extensions.breakLine
class FeedbackDataBuilder(
private val infoHolder: AdditionalFeedbackInfo
) {
@ -10,8 +12,8 @@ class FeedbackDataBuilder(
return this
}
fun appendLine(count: Int = 1): FeedbackDataBuilder {
builder.appendLine(count)
fun breakLine(count: Int = 1): FeedbackDataBuilder {
builder.breakLine(count)
return this
}
@ -34,7 +36,7 @@ class FeedbackDataBuilder(
infoHolder.tokens[it.blockchain]?.let { tokens ->
builder.append("Tokens:")
appendLine()
breakLine()
tokens.forEach { token ->
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("ID", token.id ?: "[custom token]")
@ -74,8 +76,4 @@ private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuil
return if (value.isNotBlank()) this.append("$key: $value\n") else this
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
private fun StringBuilder.appendLine(count: Int = 1): StringBuilder {
return append(List(count) { "\n" }.joinToString(separator = ""))
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")

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

@ -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

@ -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

@ -11,14 +11,20 @@ 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 +77,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)
@ -409,23 +431,43 @@ class WalletConnectManager {
Timber.d("Custom Request")
Timber.d(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
val request = EthSignHelper.parseCustomRequest(data)
when (request.method) {
WCMethodExtended.ETH_SIGN_TYPE_DATA_V4 -> handleTypedDataV4(request, client, id)
WCMethodExtended.SWITCH_CHAIN -> {
val blockchain = request.blockchainFromChainId()
Timber.d("WC switch chainID\nNew Blockchain: $blockchain")
val session = sessions[client.session]?.toWalletConnectSession()
if (session != null) {
store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session))
}
}
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

@ -5,8 +5,17 @@ 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.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
@ -17,7 +26,11 @@ import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.Analytics
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
@ -25,8 +38,8 @@ import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import timber.log.Timber
import java.math.BigDecimal
import timber.log.Timber
class WalletConnectSdkHelper {
@ -108,9 +121,8 @@ class WalletConnectSdkHelper {
)
val blockchain = session.wallet.getBlockchainForSession()
return factory.makeWalletManager(
session.wallet.cardId,
blockchain,
publicKey
blockchain = blockchain,
publicKey = publicKey
)
}
@ -124,7 +136,7 @@ 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 -> {

View file

@ -0,0 +1,95 @@
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(
cardId = wallet.cardId,
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

@ -109,7 +109,7 @@ class DemoConfig {
testDemoCardIds).distinct()
}
private val releaseDemoCardIds = mutableListOf<String>(
private val releaseDemoCardIds = mutableListOf(
// Tangem Wallet:
"AC01000000041100",
"AC01000000042462",
@ -284,7 +284,10 @@ 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.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error)

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

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.redux.walletconnect
import android.app.Activity
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.wallet.R
@ -47,6 +48,16 @@ sealed class WalletConnectAction : Action {
data class Success(val session: WalletConnectSession) : WalletConnectAction()
}
data class SwitchBlockchain(
val blockchain: Blockchain?,
val session: WalletConnectSession
) : WalletConnectAction()
data class UpdateBlockchain(
val updatedSession: WalletConnectSession
) : WalletConnectAction()
data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :

View file

@ -1,10 +1,8 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
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
@ -14,11 +12,10 @@ 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.scope
import com.tangem.tap.store
@ -159,6 +156,45 @@ class WalletConnectMiddleware {
action.id, action.data, action.sessionData
)
}
is WalletConnectAction.SwitchBlockchain -> {
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)
}
}
}
@ -189,8 +225,13 @@ class WalletConnectMiddleware {
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository
)
val walletState = store.state.walletState
scope.launch {
val walletManager = getWalletManager(scanResponse, blockchain).guard {
val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -232,37 +273,4 @@ class WalletConnectMiddleware {
}
}
private suspend 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
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card
)
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
store.state.walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork
)
} else {
null
}
}
}
}

View file

@ -34,6 +34,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

@ -82,6 +82,7 @@ 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()

View file

@ -1,10 +1,14 @@
package com.tangem.tap.features.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.AppScreen
@ -15,48 +19,36 @@ import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState> {
class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private var composeView: ComposeView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
store.dispatch(HomeAction.Init)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val context = container?.context ?: return null
store.dispatch(BackupAction.CheckForUnfinishedBackup)
getView()?.findViewById<ComposeView>(R.id.cv_stories)?.setContent {
AppCompatTheme {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = {
store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode))
},
onSearchTokensClick = {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies())
}
)
composeView = ComposeView(context).apply {
setContent {
AppCompatTheme {
ScreenContent()
}
}
}
}
private fun getRegionProvider(): RegionProvider = RegionService(
listOf(
// TelephonyManagerRegionProvider(requireContext()),
LocaleRegionProvider()
)
)
return composeView
}
override fun onStart() {
super.onStart()
@ -72,6 +64,11 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
store.unsubscribe(this)
}
override fun onDestroyView() {
super.onDestroyView()
rollbackStatusBarIconsColor()
composeView = null
}
override fun newState(state: HomeState) {
if (activity == null || view == null) return
@ -79,4 +76,32 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
homeState.value = state
}
@Composable
private fun ScreenContent() {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = {
store.dispatch(
HomeAction.GoToShop(store.state.globalState.userCountryCode)
)
},
onSearchTokensClick = {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies())
}
)
}
/*
* !!! Workaround !!!
* Used to roll back the color of icons in the status bar after the stories screen
* */
private fun rollbackStatusBarIconsColor() {
WindowInsetsControllerCompat(
activity?.window ?: return,
view ?: return,
).isAppearanceLightStatusBars = true
}
}

View file

@ -3,12 +3,25 @@ package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
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.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
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.systemBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -25,8 +38,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.features.home.compose.content.*
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.tap.features.home.compose.content.FirstStoriesContent
import com.tangem.tap.features.home.compose.content.StoriesCurrencies
import com.tangem.tap.features.home.compose.content.StoriesRevolutionaryWallet
import com.tangem.tap.features.home.compose.content.StoriesUltraSecureBackup
import com.tangem.tap.features.home.compose.content.StoriesWalletForEveryone
import com.tangem.tap.features.home.compose.content.StoriesWeb3
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
@ -43,6 +61,7 @@ fun StoriesScreen(
) {
val steps = 6
val currentStep = remember { mutableStateOf(1) }
val systemUiController = rememberSystemUiController()
val isDarkBackground = currentStep.value !in 3..5
@ -60,13 +79,20 @@ fun StoriesScreen(
val hideContent = remember { mutableStateOf(true) }
SideEffect {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = false,
)
}
Box(
Modifier
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF090E13))
) {
Row(
Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize()
) {
Box(
Modifier
@ -107,19 +133,23 @@ fun StoriesScreen(
}
if (!isDarkBackground) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.ic_overlay),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
}
val insets = WindowInsets.systemBars
.union(WindowInsets(top = 32.dp))
Column(
modifier = Modifier
.windowInsetsPadding(insets)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
SpacerS24()
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,

View file

@ -7,7 +7,7 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.ReceiptAction
@ -28,6 +28,7 @@ class RequestFeeMiddleware {
fun handle(appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val scanResponse = appState.globalState.scanResponse ?: return
if (!SendState.isReadyToRequestFee()) {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
@ -40,7 +41,7 @@ class RequestFeeMiddleware {
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = if (walletManager.isDemoWallet()) {
val txSender = if (scanResponse.isDemoCard()) {
DemoTransactionSender(walletManager)
} else {
walletManager as TransactionSender

View file

@ -4,7 +4,11 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionError
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
@ -15,7 +19,11 @@ import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -26,9 +34,15 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.MainCurrencyType
@ -38,6 +52,7 @@ import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.wallet.R
import java.util.EnumSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -45,7 +60,6 @@ import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.*
/**
[REDACTED_AUTHOR]
@ -161,7 +175,11 @@ private fun sendTransaction(
tangemSdk.config.linkedTerminal = false
}
val signer = TangemSigner(tangemSdk, action.messageForSigner) { signResponse ->
val signer = TangemSigner(
card = card,
tangemSdk = tangemSdk,
initialMessage = action.messageForSigner
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
@ -171,7 +189,7 @@ private fun sendTransaction(
)
}
val sendResult = try {
if (walletManager.isDemoWallet()) {
if (card.isDemoCard()) {
DemoTransactionSender(walletManager).send(txData, signer)
} else {
(walletManager as TransactionSender).send(txData, signer)
@ -340,5 +358,4 @@ private fun updateWarnings(dispatch: (Action) -> Unit) {
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
dispatch(SendAction.Warnings.Set(warnings))
}
}

View file

@ -73,6 +73,9 @@ data class WalletState(
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
val cardId: String?
get() = wallets.firstOrNull()?.walletManager?.wallet?.cardId
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return getWalletStore(currency)?.walletManager

View file

@ -45,6 +45,7 @@ class TradeCryptoMiddleware {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val exchangeManager = store.state.globalState.exchangeManager ?: return
val card = store.state.globalState.scanResponse?.card ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
@ -59,7 +60,13 @@ class TradeCryptoMiddleware {
return
}
scope.launch { exchangeManager.buyErc20TestnetTokens(walletManager, currency.token) }
scope.launch {
exchangeManager.buyErc20TestnetTokens(
card = card,
walletManager = walletManager,
token = currency.token
)
}
return
}

View file

@ -1,8 +1,5 @@
package com.tangem.tap.features.wallet.redux.middlewares
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
@ -15,12 +12,7 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -233,9 +225,7 @@ class WalletMiddleware {
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(action.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
store.dispatchOpenUrl(action.exploreUrl)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount, store.state.walletState)

View file

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import coil.size.Scale
import com.tangem.tap.MainActivity
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
@ -166,6 +167,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private fun setupCardImage(cardImage: Artwork?) {
binding.ivCard.load(cardImage?.artworkId) {
scale(Scale.FIT)
crossfade(enable = true)
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)

View file

@ -9,6 +9,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
@ -116,6 +117,8 @@ class WalletAdapter
?: root.getString(id = R.string.token_item_no_rate)
badgeCustomBalance.isVisible = isCustomCurrency
ivBlockchain.isVisible = wallet.currency.isToken()
ivBlockchain.setImageResource(wallet.currency.blockchain.getGreyedOutIconRes())
cardWallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))

View file

@ -5,7 +5,6 @@ import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import coil.imageLoader
import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
@ -21,13 +20,13 @@ private const val QCX = "QCX"
private const val VOYR = "VOYRME"
fun loadCurrencyIcon(
currencyImageView: ImageFilterView,
currencyImageView: CurrencyIconView,
currencyTextView: TextView,
token: Token?,
blockchain: Blockchain,
) {
CurrencyIconLoader(
currencyImageView = currencyImageView,
currencyImageView = currencyImageView.imageView,
currencyTextView = currencyTextView,
token = token,
blockchain = blockchain
@ -135,14 +134,6 @@ private inline fun ImageView.loadIcon(
.placeholder(placeholderRes)
.error(placeholderRes)
.fallback(placeholderRes)
.transformations(
RoundedCornersTransformation(
topLeft = 32f,
topRight = 32f,
bottomLeft = 32f,
bottomRight = 32f
)
)
.listener(
onStart = { onStart() },
onSuccess = { _, _ -> onSuccess() },

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.wallet.ui.images
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.utils.widget.ImageFilterView
import com.google.android.material.card.MaterialCardView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.wallet.databinding.ViewCurrencyIconBinding
class CurrencyIconView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : MaterialCardView(context, attrs, defStyleAttr) {
private val binding = ViewCurrencyIconBinding.inflate(
LayoutInflater.from(context),
this
)
val imageView: ImageFilterView
get() = binding.iv
init {
elevation = 0f
cardElevation = 0f
radius = dpToPx(8f)
background = null
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
@ -68,7 +69,11 @@ class CurrencyExchangeManager(
enum class Action { Buy, Sell }
}
suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: EthereumWalletManager, token: Token) {
suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(
card: Card,
walletManager: EthereumWalletManager,
token: Token,
) {
walletManager.safeUpdate()
val amountToSend = Amount(walletManager.wallet.blockchain)
@ -85,7 +90,9 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: Ethereu
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
val signer = TangemSigner(
tangemSdk = tangemSdk, Message()
card = card,
tangemSdk = tangemSdk,
initialMessage = Message(),
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(

View file

@ -4,10 +4,18 @@
<path android:fillColor="#F8F9F9" android:pathData="M0,0h360v720h-360z"/>
<path android:fillAlpha="0.2" android:pathData="M0,0h360v720h-360z">
<aapt:attr name="android:fillColor">
<gradient android:endX="179.591" android:endY="82.4634"
android:startX="180" android:startY="0" android:type="linear">
<item android:color="#FF090E13" android:offset="0"/>
<item android:color="#00000000" android:offset="1"/>
<gradient
android:endX="179.591"
android:endY="120.4634"
android:startX="180"
android:startY="0"
android:type="linear">
<item
android:color="#FF090E13"
android:offset="0" />
<item
android:color="#00000000"
android:offset="1" />
</gradient>
</aapt:attr>
</path>

View file

@ -7,7 +7,7 @@
android:height="12dp" />
<stroke
android:width="2dp"
android:width="1dp"
android:color="@color/white" />
<solid android:color="@color/darkGray1" />

View file

@ -5,4 +5,4 @@
<corners android:radius="8dp" />
</shape>
</shape>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/cv_stories"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -18,14 +18,14 @@
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/wallet"
app:navigationIcon="@drawable/ic_wallet_24"
app:navigationIconTint="@color/darkGray6"
app:titleCentered="true"
app:title="@string/wallet_title" />
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/wallet"
app:navigationIcon="@drawable/ic_wallet_24"
app:navigationIconTint="@color/darkGray6"
app:title="@string/wallet_title"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>
@ -43,44 +43,44 @@
android:overScrollMode="never">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="74dp">
android:id="@+id/cl_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="74dp">
<ImageView
android:id="@+id/iv_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:adjustViewBounds="true"
android:elevation="3dp"
android:maxHeight="215dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_black"
android:contentDescription="@null"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/iv_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:elevation="3dp"
android:maxHeight="215dp"
android:scaleType="fitCenter"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/card_placeholder_black" />
<TextView
android:id="@+id/tv_twin_card_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="30dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginStart="25dp"
android:background="@drawable/shape_chip"
android:elevation="3dp"
android:gravity="center"
android:layout_marginStart="25dp"
android:minHeight="30dp"
android:paddingStart="16dp"
android:paddingTop="4dp"
android:paddingEnd="16dp"
android:paddingBottom="4dp"
android:textColor="@color/buttonGray"
android:textSize="14sp"
android:textStyle="bold"
android:background="@drawable/shape_chip"
app:layout_constraintBottom_toBottomOf="@id/iv_card"
app:layout_constraintStart_toStartOf="@id/iv_card"
app:layout_constraintTop_toBottomOf="@id/iv_card"
@ -95,50 +95,50 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_warning_messages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:minHeight="0dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<include
android:id="@+id/l_card_total_balance"
layout="@layout/layout_card_total_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:visibility="gone"
app:layout_goneMarginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/l_wallet_backup_warning" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_goneMarginTop="12dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:minHeight="0dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_card_total_balance" />
app:layout_constraintTop_toBottomOf="@id/barrier" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<include
android:id="@+id/l_card_total_balance"
layout="@layout/layout_card_total_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/l_wallet_backup_warning"
app:layout_goneMarginTop="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_card_total_balance"
app:layout_goneMarginTop="12dp" />
<include
android:id="@+id/l_card_balance"
@ -148,7 +148,6 @@
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<include
android:id="@+id/l_address"
layout="@layout/layout_address"
@ -158,60 +157,59 @@
app:layout_constraintTop_toBottomOf="@id/l_card_balance" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_multiwallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
android:id="@+id/rv_multiwallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<include
android:id="@+id/l_buttons_long"
layout="@layout/layout_wallet_long_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="30dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1"
app:layout_constraintBottom_toBottomOf="parent" />
android:id="@+id/l_buttons_long"
layout="@layout/layout_wallet_long_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1" />
<include
android:id="@+id/l_buttons_short"
layout="@layout/layout_wallet_short_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="30dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1"
app:layout_constraintBottom_toBottomOf="parent" />
android:id="@+id/l_buttons_short"
layout="@layout/layout_wallet_short_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_token"
style="@style/BaseTapButton"
android:layout_width="match_parent"
android:layout_height="42dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:elevation="4dp"
android:text="@string/main_manage_tokens"
android:textSize="16sp"
app:cornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_multiwallet"
app:layout_constraintVertical_bias="0.0"
tools:layout_editor_absoluteX="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_token"
style="@style/BaseTapButton"
android:layout_width="match_parent"
android:layout_height="42dp"
android:layout_gravity="bottom"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textSize="16sp"
android:text="@string/main_manage_tokens"
android:elevation="4dp"
app:cornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -16,45 +16,62 @@
android:elevation="3dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:animateLayoutChanges="true">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:animateLayoutChanges="true"
android:clipToPadding="false">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_currency"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/shape_circle" />
<com.tangem.tap.features.wallet.ui.images.CurrencyIconView
android:id="@+id/iv_currency"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_token_letter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:textColor="@android:color/white"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
android:id="@+id/tv_token_letter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:textColor="@android:color/white"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
app:layout_constraintEnd_toEndOf="@id/iv_currency"
app:layout_constraintStart_toStartOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/iv_currency"
tools:text="J" />
<ImageView
android:id="@+id/iv_blockchain"
android:layout_width="16dp"
android:layout_height="16dp"
android:padding="1dp"
android:translationX="4dp"
android:translationY="-4dp"
android:background="@drawable/shape_circle"
android:backgroundTint="@color/backgroundWhite"
android:contentDescription="@null"
android:visibility="gone"
app:layout_constraintTop_toTopOf="@id/iv_currency"
app:layout_constraintEnd_toEndOf="@id/iv_currency"
tools:src="@drawable/ic_eth_no_color"
tools:visibility="visible" />
<View
android:id="@+id/badge_custom_balance"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/shape_badge_custom_currency"
android:visibility="gone"
app:layout_constraintTop_toTopOf="@id/iv_currency"
app:layout_constraintEnd_toEndOf="@id/iv_currency" />
android:id="@+id/badge_custom_balance"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@drawable/shape_badge_custom_currency"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
app:layout_constraintEnd_toEndOf="@id/iv_currency"
tools:visibility="visible" />
<include
layout="@layout/item_currency_wallet_content"

View file

@ -39,14 +39,13 @@
app:layout_constraintTop_toBottomOf="@id/tv_fiat_amount"
tools:text="0.43 BTC" />
<androidx.constraintlayout.utils.widget.ImageFilterView
<com.tangem.tap.features.wallet.ui.images.CurrencyIconView
android:id="@+id/iv_currency"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/shape_circle" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_token_letter"
@ -100,4 +99,4 @@
app:constraint_referenced_ids="tv_amount, tv_fiat_amount, tv_status" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@null"
android:elevation="0dp"
app:cardCornerRadius="8dp"
app:cardElevation="0dp"
tools:parentTag="androidx.cardview.widget.CardView">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
tools:src="@drawable/ic_eth" />
</merge>

View file

@ -59,6 +59,7 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
<string name="security_and_privacy_title">Security &amp; Privacy</string>

View file

@ -59,6 +59,7 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
<string name="security_and_privacy_title">Security &amp; Privacy</string>

View file

@ -59,6 +59,8 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
<string name="security_and_privacy_title">Security &amp; Privacy</string>

View file

@ -61,9 +61,12 @@
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно.</string>
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
<string name="token_details_unable_hide_alert_message">Токен %s является основной валютой в сети %s и не может быть скрыт, до тех пор пока у вас в списке есть другие токены этой сети.</string>
<string name="token_details_unable_hide_alert_message">Токен %s является основной валютой в сети %s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
<string name="wallet_connect_network_not_found_format">Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново.</string>
<string name="wallet_connect_scanner_error_unsupported_network">Сеть не поддерживается. Пожалуйста, выберите другую сеть.</string>
<string name="wallet_currency_subtitle">Сеть %s</string>
<string name="main_no_backup_warning_title">Бэкап кошелька не был произведен</string>

View file

@ -64,6 +64,8 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
<string name="main_no_backup_warning_title">Your wallet has not been backed up</string>

View file

@ -5,10 +5,6 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorSecondary">@color/colorSecondary</item>
<item name="android:textColor">@color/darkGray6</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="actionMenuTextColor">@color/menu_accent_color</item>
<item name="materialCardViewStyle">@style/Widget.AppTheme.CardView</item>

View file

@ -4,4 +4,15 @@
<item name="colorPrimary">@color/darkGray6</item>
<item name="colorPrimaryDark">@color/darkGray6</item>
</style>
// Contact us FAB style
<style name="zs_contact_us_fab">
<item name="android:gravity">bottom|end</item>
<item name="android:layout_margin">@dimen/zs_fab_margin</item>
<item name="android:contentDescription">@string/zs_general_contact_us_button_label_accessibility</item>
<item name="android:visibility">gone</item>
<item name="backgroundTint">@color/selector_btn_green</item>
<item name="srcCompat">@drawable/zs_create_ticket_fab</item>
<item name="tint">@color/white</item>
</style>
</resources>

View file

@ -1,12 +1,12 @@
ext.versions = [
kotlin : '1.6.10',
build_gradle : '7.1.3',
tamgem_card_sdk : 'develop-154',
tamgem_blockchain_sdk: 'develop-93',
tamgem_card_sdk : 'develop-156',
tamgem_blockchain_sdk: 'develop-96',
]
ext.environmentConfig = [
environment : "ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
logEnabled : "LOG_ENABLED",
]
]

View file

@ -85,4 +85,4 @@ dependencies {
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
}