Updated on 2026-08-14

This commit is contained in:
Tangem 2021-03-17 16:13:28 +03:00
commit 427fe69313
72 changed files with 1305 additions and 159 deletions

View file

@ -40,6 +40,7 @@ android {
initWith debug
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
}
}
kotlinOptions {
@ -69,8 +70,11 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.3.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "androidx.core:core-ktx:1.3.2"
implementation 'com.google.android.play:core:1.9.1'
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// implementation 'com.tangem:blockchain:1.141.0'
implementation 'blockchain-sdk-kotlin:blockchain:tangem-20210302.084810-3'

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">DEBUG</string>
<string name="app_name" translatable="false">DebugTangem</string>
</resources>

View file

@ -81,6 +81,17 @@
</activity>
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

View file

@ -1,6 +1,6 @@
{
"isWalletPayIdEnabled": true,
"isTopUpEnabled": true,
"isSendingToPayIdEnabled": true,
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": false
}

View file

@ -1,6 +1,6 @@
{
"isWalletPayIdEnabled": true,
"isTopUpEnabled": true,
"isWalletPayIdEnabled": false,
"isSendingToPayIdEnabled": true,
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -4,6 +4,7 @@ import android.app.Application
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.tap.common.images.PicassoHelper
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
@ -14,6 +15,9 @@ import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
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.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.network.createMoshi
import com.tangem.tap.persistence.PreferencesStorage
@ -49,10 +53,10 @@ class TapApplication : Application() {
PicassoHelper.initPicassoWithCaching(this)
currenciesRepository = CurrenciesRepository(this)
initFeedbackManager()
loadConfigs()
}
private fun loadConfigs() {
val moshi = createMoshi()
val localLoader = FeaturesLocalLoader(this, moshi)
@ -62,4 +66,15 @@ class TapApplication : Application() {
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
}
private fun initFeedbackManager() {
val infoHolder = AdditionalEmailInfo()
infoHolder.updateAppVersion(this)
val logWriter = TangemLogCollector()
Log.addLogger(logWriter)
val feedbackManager = FeedbackManager(infoHolder, this, logWriter)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}
}

View file

@ -4,4 +4,8 @@ enum class AnalyticsEvent(val event: String) {
CARD_IS_SCANNED("card_is_scanned"),
TRANSACTION_IS_SENT("transaction_is_sent"),
READY_TO_SCAN("ready_to_scan"),
APP_RATING_DISPLAYED("rate_app_warning_displayed"),
APP_RATING_DISMISS("dismiss_rate_app_warning"),
APP_RATING_NEGATIVE("negative_rate_app_feedback"),
APP_RATING_POSITIVE("positive_rate_app_feedback"),
}

View file

@ -4,5 +4,5 @@ import com.tangem.commands.common.card.Card
interface AnalyticsHandler {
fun triggerEvent(event: AnalyticsEvent, card: Card?)
fun triggerEvent(event: AnalyticsEvent, card: Card? = null)
}

View file

@ -8,8 +8,7 @@ import com.tangem.commands.common.card.Card
object FirebaseAnalyticsHandler : AnalyticsHandler {
override fun triggerEvent(event: AnalyticsEvent, card: Card?) {
Firebase.analytics
.logEvent(event.event, setCardData(card))
Firebase.analytics.logEvent(event.event, setCardData(card))
}
fun logException(name: String, throwable: Throwable) {

View file

@ -5,6 +5,8 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
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 {
@ -15,8 +17,12 @@ sealed class GlobalAction : Action {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
}
data class UpdateWalletSignedHashes(val walletSignedHashes: Int?) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager): GlobalAction()
data class SendFeedback(val emailData: EmailData): GlobalAction()
}

View file

@ -26,11 +26,17 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
store.dispatch(WalletAction.CheckSignedHashes.SaveCardId)
}
store.dispatch(WalletAction.SetWarnings(it.getWarnings(WarningMessage.Location.MainScreen)))
store.dispatch(SendAction.SetWarnings(it.getWarnings(WarningMessage.Location.SendScreen)))
store.dispatch(WalletAction.Warnings.SetWarnings(
it.getWarnings(WarningMessage.Location.MainScreen)))
store.dispatch(SendAction.SetWarnings(
it.getWarnings(WarningMessage.Location.SendScreen)))
}
}
}
is GlobalAction.SendFeedback -> {
store.state.globalState.feedbackManager?.send(action.emailData)
}
}
nextDispatch(action)
}

View file

@ -52,6 +52,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState
}
}
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
}
else -> globalState
}
}

View file

@ -7,6 +7,7 @@ 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.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import org.rekotlin.StateType
@ -18,6 +19,7 @@ data class GlobalState(
val tangemService: TangemService = TangemService(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY
) : StateType

View file

@ -49,7 +49,7 @@ sealed class TapError(
}
sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError {
final override val code: Int = 1
final override val code: Int = 50100
override var customMessage: String = code.toString()
object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card)

View file

@ -85,20 +85,10 @@ class TapWalletManager {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card)
}
TapWorkarounds.updateCard(data.card)
store.state.globalState.warningManager?.setBlockchain(data.walletManager?.wallet?.blockchain)
val configManager = store.state.globalState.configManager
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
}
updateConfigManager(data)
updateFeedbackManager(data)
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
@ -120,9 +110,38 @@ class TapWalletManager {
}
}
private fun updateConfigManager(data: ScanNoteResponse) {
val configManager = store.state.globalState.configManager
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
}
}
private fun updateFeedbackManager(data: ScanNoteResponse) {
val card = data.card
val wallet = data.walletManager?.wallet ?: return
val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return
infoHolder.cardId = card.cardId
infoHolder.cardFirmwareVersion = card.firmwareVersion.version
infoHolder.signedHashesCount = card.walletSignedHashes?.toString() ?: "0"
infoHolder.sourceAddress = wallet.address
infoHolder.explorerLink = wallet.getExploreUrl(wallet.address)
infoHolder.blockchain = wallet.blockchain
}
suspend fun loadData(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.CheckSignedHashes.CheckIfWarningNeeded)
//TODO: I made it to WalletAction.CheckSignedHashes.CheckIfWarningNeeded
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
val artworkId = data.verifyResponse?.artworkInfo?.id
if (data.walletManager != null) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.masks.Product
import java.util.*
object TapWorkarounds {
@ -12,5 +13,26 @@ object TapWorkarounds {
isStart2Coin = card.cardData?.issuerName?.toLowerCase(Locale.US) == START_2_COIN_ISSUER
}
const val START_2_COIN_ISSUER = "start2coin"
fun Card.isExcluded(): Boolean {
val cardData = this.cardData ?: return false
val productMask = cardData.productMask
val excludedBatch = excludedBatches.contains(cardData.batchId)
val excludedIssuerName = excludedIssuers.contains(cardData.issuerName?.capitalize(Locale.US))
val excludedProductMask = (productMask != null && // product mask is on cards v2.30 and later
!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard))
return excludedBatch || excludedIssuerName || excludedProductMask
}
private const val START_2_COIN_ISSUER = "start2coin"
private val excludedBatches = listOf(
"0027",
"0030",
"0031",
) // Tangem tags
private val excludedIssuers = listOf(
"TTM BANK"
)
}

View file

@ -40,7 +40,9 @@ data class WarningMessage(
Permanent, // нельзя скрыть
@Json(name = "temporary")
Temporary // можно скрыть (кнопка ОК)
Temporary, // можно скрыть (кнопка ОК)
AppRating
}
enum class Location {

View file

@ -51,7 +51,8 @@ class WarningMessagesManager(
val foundWarning = findWarning(warning)
return when {
foundWarning == null -> false
foundWarning.type == WarningMessage.Type.Temporary -> {
foundWarning.type == WarningMessage.Type.Temporary
|| foundWarning.type == WarningMessage.Type.AppRating -> {
if (foundWarning.isHidden) {
false
} else {
@ -101,7 +102,19 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun isAlreadySignedHashesWarning(warning: WarningMessage):Boolean {
fun appRatingWarning(): WarningMessage = WarningMessage(
"",
"",
WarningMessage.Type.AppRating,
WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local
)
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
return warning.messageResId == R.string.alert_card_signed_transactions
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.store
@ -123,23 +124,13 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
val productMask = card.cardData?.productMask
if (productMask != null && // product mask is on cards v2.30 and later
!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard)) {
return TapSdkError.CardForDifferentApp
}
if (excludedBatches.contains(card.cardData?.batchId)) {
return TapSdkError.CardForDifferentApp
}
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
if (card.status == CardStatus.Purged) return TangemSdkError.CardIsPurged()
if (card.status == CardStatus.NotPersonalized) return TangemSdkError.NotPersonalized()
return null
}
companion object {
private val excludedBatches = listOf("0027", "0030", "0031")
}
}
val Card.isMultiwalletAllowed: Boolean

View file

@ -0,0 +1,46 @@
package com.tangem.tap.domain.termsOfUse
import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat
import com.tangem.commands.common.card.Card
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class CardTou {
private val locale: Locale = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)
fun getUrl(card: Card): Uri? {
val issuerName = card.cardData?.issuerName ?: return null
if (issuerName.toLowerCase() != "start2coin") return null
val baseUrl = "https://app.tangem.com/tou/"
val regionCode = regionCode(card.cardId) ?: "fr"
val filename = filename(locale.language, regionCode)
return Uri.parse(baseUrl + filename)
}
private fun filename(languageCode: String, regionCode: String): String {
return when {
languageCode == "fr" && regionCode == "ch" -> "Start2Coin-fr-ch-tangem.pdf"
languageCode == "de" && regionCode == "ch" -> "Start2Coin-de-ch-tangem.pdf"
languageCode == "en" && regionCode == "ch" -> "Start2Coin-en-ch-tangem.pdf"
languageCode == "it" && regionCode == "ch" -> "Start2Coin-it-ch-tangem.pdf"
languageCode == "fr" && regionCode == "fr" -> "Start2Coin-fr-fr-atangem.pdf"
languageCode == "de" && regionCode == "at" -> "Start2Coin-de-at-tangem.pdf"
regionCode == "fr" -> "Start2Coin-fr-fr-atangem.pdf"
regionCode == "ch" -> "Start2Coin-en-ch-tangem.pdf"
regionCode == "at" -> "Start2Coin-de-at-tangem.pdf"
else -> "Start2Coin-fr-fr-atangem.pdf"
}
}
private fun regionCode(cardId: String): String? = when (cardId[1]) {
'0' -> "fr"
'1' -> "ch"
'2' -> "at"
else -> null
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.network.coinmarketcap.FiatCurrency
@ -19,6 +20,7 @@ sealed class DetailsAction : Action {
val scanNoteResponse: ScanNoteResponse,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
) : DetailsAction()

View file

@ -75,6 +75,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
),
securityScreenState = SecurityScreenState(currentOption = securityOption),
createTwinWalletState = twinsState,
cardTermsOfUseUrl = action.cardTou.getUrl(action.card)
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.entities.Button
@ -18,13 +19,14 @@ data class DetailsState(
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val createTwinWalletState: CreateTwinWalletState? = null
val createTwinWalletState: CreateTwinWalletState? = null,
val cardTermsOfUseUrl: Uri? = null,
) : StateType
data class CardInfo(
val cardId: String,
val issuer: String,
val signedHashes: Int
val signedHashes: Int,
)
enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty }
@ -33,7 +35,7 @@ data class SecurityScreenState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true)
val buttonProceed: Button = Button(true),
)
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -18,7 +18,7 @@ class CreateTwinWalletMiddleware {
fun handle(action: DetailsAction.CreateTwinWalletAction) {
when (action) {
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
val wallet = store.state.detailsState.wallets.getOrNull(0)
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
if (wallet == null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
return

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
@ -7,6 +8,7 @@ import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.getTwinCardIdForUser
import com.tangem.tap.domain.twins.isTwinCard
@ -14,6 +16,7 @@ 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.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details.*
@ -79,6 +82,13 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tv_card_tou.show(state.cardTermsOfUseUrl != null)
tv_card_tou.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = state.cardTermsOfUseUrl
startActivity(intent)
}
if (state.createTwinWalletState != null) {
if (state.createTwinWalletState.allowRecreatingWallet == true) {
tv_erase_wallet.show()
@ -106,6 +116,9 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_app_currency_title.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
}
tv_send_feedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
tv_security_title.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)

View file

@ -0,0 +1,206 @@
package com.tangem.tap.features.feedback
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.LogMessage
import com.tangem.LoggerInterface
import com.tangem.blockchain.common.Blockchain
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
/**
[REDACTED_AUTHOR]
*/
class FeedbackManager(
val infoHolder: AdditionalEmailInfo,
private val context: Context,
private val logCollector: TangemLogCollector,
private val email: String = "support@tangem.com",
) {
fun send(emailData: EmailData) {
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
sendTo(email, emailData.subject, emailData.joinTogether(infoHolder), fileLog)
}
private fun sendTo(email: String, subject: String, message: String, fileLog: File? = null) {
val intent = Intent(Intent.ACTION_SEND).apply {
data = Uri.parse("mailto:")
type = "text/plain"
putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, message)
fileLog?.let {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", it)
putExtra(Intent.EXTRA_STREAM, uri)
}
}
try {
val chooserIntent = Intent.createChooser(intent, "Send mail...")
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ContextCompat.startActivity(context, chooserIntent, null)
} catch (ex: ActivityNotFoundException) {
Timber.e(ex)
}
}
private fun createLogFile(): 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 a file for email attachment")
null
}
}
}
class TangemLogCollector : LoggerInterface {
private val logs = mutableListOf<String>()
override fun e(logTag: String, message: String) {}
override fun i(logTag: String, message: String) {}
override fun v(logTag: String, message: String) {}
override fun write(message: LogMessage) {
logs.add(message.message)
}
fun getLogs(): List<String> = logs.toList()
fun clearLogs() {
logs.clear()
}
}
class AdditionalEmailInfo {
var cardId: String = ""
var cardFirmwareVersion: String = ""
var blockchain: Blockchain = Blockchain.Unknown
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
var appVersion: String = ""
var token: String = ""
var sourceAddress: String = ""
var destinationAddress: String = ""
var amount: String = ""
var fee: String = ""
// var transactionHex: String = ""
var signedHashesCount: String = ""
var explorerLink: String = ""
// var outputsCount: String = ""
fun updateAppVersion(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
}
interface EmailData {
val subject: String
val mainMessage: String
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
fun joinTogether(infoHolder: AdditionalEmailInfo): String {
return "$mainMessage\n\n\n\n\n" +
"Following information is optional. You can erase it if you dont want to share it.\n" +
createOptionalMessage(infoHolder)
}
}
class RateCanBeBetterEmail : EmailData {
override val subject: String = "My suggestions"
override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
}.toString()
}
}
class ScanFailsEmail : EmailData {
override val subject: String = "Cant scan a card"
override val mainMessage: String = "Please tell us what card do you have?"
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
}.toString()
}
}
class SendTransactionFailedEmail(private val error: String) : EmailData {
override val subject: String = "Cant send a transaction"
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Error", error)
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Token", infoHolder.token)
appendKeyValue("Source address", infoHolder.sourceAddress)
appendKeyValue("Destination address", infoHolder.destinationAddress)
appendKeyValue("Amount", infoHolder.amount)
appendKeyValue("Fee", infoHolder.fee)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
// appendKeyValue("Transaction HEX", infoHolder.transactionHex)
}.toString()
}
}
class FeedbackEmail : EmailData {
override val subject: String = "Tangem Tap feedback"
override val mainMessage: String = "Hi Tangem,"
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Wallet address", infoHolder.sourceAddress)
appendKeyValue("Explorer link", infoHolder.explorerLink)
// appendKeyValue("Outputs count", infoHolder.outputsCount)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
}.toString()
}
}
fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
return this.append("$key: $value\n")
}

View file

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.store
@ -30,6 +31,9 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
store.dispatch(NavigationAction.PopBackTo())
}
})
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? {

View file

@ -141,12 +141,12 @@ sealed class SendAction : SendScreenAction {
data class SendError(override val error: TapError) : SendAction(), ErrorAction
sealed class Dialog : SendAction() {
data class ShowTezosWarningDialog(
data class TezosWarningDialog(
val reduceCallback: () -> Unit,
val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal,
) : Dialog()
data class SendTransactionFails(val errorMessage: String): Dialog()
object Hide : Dialog()
}
data class SetWarnings(val warningList: List<WarningMessage>) : SendAction()

View file

@ -1,19 +1,17 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.network.Result
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.isPayIdSupported
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -50,7 +48,7 @@ internal class AddressPayIdMiddleware {
private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val potentialPayId = data.toLowerCase()
if (PayIdManager.isPayId(potentialPayId) && isPayIdEnabled()) {
if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) {
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
} else {
dispatch(SetWalletAddress(data, isUserInput))
@ -64,7 +62,7 @@ internal class AddressPayIdMiddleware {
val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return
val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
if (isPayIdEnabled() && PayIdManager.isPayId(addressPayId)) {
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
} else {
verifyAddress(addressPayId, wallet, isUserInput, dispatch)
@ -110,7 +108,11 @@ internal class AddressPayIdMiddleware {
}
private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val addressSchemeSplit = address.split(":")
val addressSchemeSplit = if (wallet.blockchain == Blockchain.BitcoinCash) {
listOf(address)
} else {
address.split(":")
}
val noSchemeAddress = when (addressSchemeSplit.size) {
1 -> address // no scheme
2 -> { // scheme
@ -131,6 +133,9 @@ internal class AddressPayIdMiddleware {
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress)
if (failReason == null) {
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
} else {
@ -141,7 +146,7 @@ internal class AddressPayIdMiddleware {
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.addresses.all { it.value != address } ) {
if (wallet.addresses.all { it.value != address }) {
null
} else {
Error.ADDRESS_SAME_AS_WALLET
@ -151,14 +156,10 @@ internal class AddressPayIdMiddleware {
}
}
//TODO: move to the blockchainSDK
private fun extractAddressFromShareUri(shareUri: String): String {
val sharePrefix = listOf("bitcoin:", "ethereum:", "xrpl:", "litecoin:", "bnb:")
val prefixes = sharePrefix.filter { shareUri.contains(it) }
return if (prefixes.isEmpty()) shareUri else shareUri.replace(prefixes[0], "")
}
private fun String.removeShareUriQuery(): String = this.substringBefore("?")
private fun String.getQueryParameter(name: String): String? {
return this.substringAfter("?").splitToMap("&", "=")[name]
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val addressPayId = input ?: return
@ -186,4 +187,11 @@ internal class AddressPayIdMiddleware {
private fun isPayIdEnabled(): Boolean {
return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false
}
}
fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map<String, String> {
return this.split(firstDelimiter)
.map { it.split(secondDelimiter) }
.map { it.first() to it.last().toString() }
.toMap()
}

View file

@ -22,6 +22,7 @@ import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -52,7 +53,7 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
}
private fun verifyAndSendTransaction(
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return
@ -68,7 +69,7 @@ private fun verifyAndSendTransaction(
when {
hadTezosError -> {
val reduceAmount = walletManager.wallet.blockchain.minimalAmount()
dispatch(SendAction.Dialog.ShowTezosWarningDialog(reduceCallback = {
dispatch(SendAction.Dialog.TezosWarningDialog(reduceCallback = {
dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false))
dispatch(AmountActionUi.CheckAmountToSend)
}, sendAllCallback = {
@ -94,7 +95,7 @@ private fun sendTransaction(
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: Card,
dispatch: (Action) -> Unit
dispatch: (Action) -> Unit,
) {
dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS))
var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress)
@ -144,6 +145,8 @@ private fun sendTransaction(
when {
message == null -> {
dispatch(SendAction.SendError(TapError.UnknownError))
updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card)
dispatch(SendAction.Dialog.SendTransactionFails("unknown error"))
}
message.contains("50002") -> {
// user was cancelled the operation by closing the Sdk bottom sheet
@ -157,6 +160,8 @@ private fun sendTransaction(
Timber.e(throwable)
FirebaseCrashlytics.getInstance().recordException(throwable)
dispatch(SendAction.SendError(TapError.CustomError(message)))
updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card)
dispatch(SendAction.Dialog.SendTransactionFails(message))
}
}
}
@ -168,6 +173,29 @@ private fun sendTransaction(
}
}
private fun updateFeedbackManager(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
card: Card,
) {
val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return
val amountState = store.state.sendState.amountState
infoHolder.cardId = card.cardId
infoHolder.blockchain = walletManager.wallet.blockchain
infoHolder.sourceAddress = walletManager.wallet.address
infoHolder.destinationAddress = destinationAddress
infoHolder.amount = amountToSend.value?.stripZeroPlainString() ?: "0"
infoHolder.fee = feeAmount.value?.stripZeroPlainString() ?: "0"
infoHolder.cardFirmwareVersion = card.firmwareVersion.version
if (amountState.typeOfAmount is AmountType.Token) {
infoHolder.token = amountState.amountToExtract?.currencySymbol ?: ""
}
// infoHolder.transactionHex = ""
}
fun extractErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<TransactionError> {
val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java)
errors.forEach {

View file

@ -42,7 +42,8 @@ private class SendReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val result = when (action) {
is SendAction.ChangeSendButtonState -> sendState.copy(sendButtonState = action.state)
is SendAction.Dialog.ShowTezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
is SendAction.SetWarnings -> sendState.copy(sendWarningsList = action.warningList)
else -> return sendState

View file

@ -149,9 +149,14 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: ""
if (scannedCode.isEmpty()) return
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
imvQrCode.postDelayed({
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
}, 200)
}
private fun setupAmountLayout() {

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.feedback.SendTransactionFailedEmail
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class SendTransactionFailsDialog {
companion object {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, dialog.errorMessage))
setNeutralButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendFeedback(SendTransactionFailedEmail(dialog.errorMessage)))
}
setPositiveButton(R.string.common_no) { _, _ -> }
setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
}.create()
}
}
}

View file

@ -9,10 +9,10 @@ import com.tangem.wallet.R
class TezosWarningDialog(context: Context) : AlertDialog(context) {
companion object {
fun create(context: Context, showDialogData: SendAction.Dialog.ShowTezosWarningDialog): AlertDialog {
fun create(context: Context, showDialogData: SendAction.Dialog.TezosWarningDialog): AlertDialog {
val reduceAmount = showDialogData.reduceAmount.toPlainString()
return Builder(context).apply {
setTitle(context.getString(R.string.common_warning))
setTitle(R.string.common_warning)
setMessage(context.getString(R.string.xtz_withdrawal_message_warning, reduceAmount))
setNegativeButton(R.string.xtz_withdrawal_message_ignore) { _, _ ->
showDialogData.sendAllCallback()

View file

@ -21,6 +21,7 @@ import com.tangem.tap.features.send.redux.reducers.ReceiptReducer
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.store
@ -97,12 +98,18 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val sendFragment = (fg as? SendFragment) ?: return
when (state.dialog) {
is SendAction.Dialog.ShowTezosWarningDialog -> {
is SendAction.Dialog.TezosWarningDialog -> {
if (dialog == null) {
dialog = TezosWarningDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.SendTransactionFails -> {
if (dialog == null) {
dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
else -> {
dialog?.dismiss()
dialog = null
@ -184,22 +191,6 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val amountToSend = state.viewAmountValue
if (!amountToSend.isFromUserInput) fg.etAmountToSend.update(amountToSend.value)
// fg.tvAmountToSendShadow.text = amountToSend
// if (amountToSend.length > 10) {
// post is needed to wait for text size changes
// fg.tvAmountToSendShadow.post {
// fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, fg.tvAmountToSendShadow.textSize - 2)
// fg.etAmountToSend.update(amountToSend)
// if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length)
// }
// } else {
// val textSize = fg.resources.getDimension(R.dimen.text_size_amount_to_send)
// fg.tvAmountToSendShadow.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
// fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
// fg.etAmountToSend.update(amountToSend)
// if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length)
// }
fg.tvAmountCurrency.update(state.mainCurrency.currencySymbol)
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)

View file

@ -2,6 +2,9 @@ package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.*
import com.tangem.TangemError
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.NotificationAction
@ -30,7 +33,8 @@ sealed class WalletAction : Action {
data class SetArtworkId(val artworkId: String?) : WalletAction()
// sealed class ProcessWallet : WalletAction() {
// sealed class ProcessWallet : WalletAction() {
// data class LoadWallet(val artworkId: String?, val allowTopUp: Boolean
// ) : ProcessWallet() {
// data class Success(val wallet: Wallet) : ProcessWallet()
@ -45,7 +49,6 @@ sealed class WalletAction : Action {
// }
// }
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddWalletManagers(val walletManagers: List<WalletManager>) : MultiWallet()
@ -68,7 +71,15 @@ sealed class WalletAction : Action {
object SaveCardId : CheckSignedHashes()
}
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
object Warnings : WalletAction() {
object CheckIfNeeded : WalletAction()
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
object AppRating : WalletAction() {
object SetNeverToShow : WalletAction()
object RemindLater : WalletAction()
}
}
data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() {
object ScheduleUpdatingWallet : WalletAction()
@ -88,40 +99,59 @@ sealed class WalletAction : Action {
object Failure : WalletAction()
}
data class CopyAddress(val address: String, val context: Context) : WalletAction() {
object Success : WalletAction(), NotificationAction {
override val messageResource = R.string.wallet_notification_address_copied
}
}
data class ShareAddress(val address: String, val context: Context) : WalletAction()
object ShowQrCode : WalletAction()
object HideDialog : WalletAction()
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
object Scan : WalletAction()
class ScanCardFinished(val scanError: TangemError? = null) : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()
}
object CreatePayId : WalletAction() {
data class CompleteCreatingPayId(val payId: String) : WalletAction()
data class Success(val payId: String) : WalletAction()
object EmptyField : WalletAction(), ErrorAction {
override val error = TapError.PayIdEmptyField
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
data class CopyAddress(val address: String, val context: Context) : WalletAction() {
object Success : WalletAction(), NotificationAction {
override val messageResource = R.string.wallet_notification_address_copied
}
}
sealed class TwinsAction : WalletAction() {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean
) : TwinsAction()
}
}
data class ShareAddress(val address: String, val context: Context) : WalletAction()
object ShowDialog : WalletAction() {
object QrCode : WalletAction()
object ScanFails : WalletAction()
}
object HideDialog : WalletAction()
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
object Scan : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
sealed class TwinsAction : WalletAction() {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean,
) : TwinsAction()
}
}

View file

@ -0,0 +1,356 @@
package com.tangem.tap.features.wallet.redux
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.getType
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TopUpHelper
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.math.BigDecimal
class WalletMiddleware {
private val topUpMiddleware = TopUpMiddleware()
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is WalletAction.TopUpAction -> topUpMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
store.state.globalState.tapWalletManager.loadWalletData()
}
}
is WalletAction.LoadPayId -> {
scope.launch {
store.state.globalState.tapWalletManager.loadPayId()
}
}
is WalletAction.LoadFiatRate -> {
scope.launch {
store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency)
}
}
is WalletAction.CreateWallet -> {
if (store.state.walletState.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
store.state.globalState.scanNoteResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
CreateTwinWallet.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager
.onCardScanned(result.data)
}
}
}
}
}
is WalletAction.UpdateWallet -> {
if (store.state.walletState.state == ProgressState.Done) {
scope.launch { store.state.globalState.tapWalletManager.updateWallet() }
}
}
is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet)
is WalletAction.LoadWallet.Success -> {
store.dispatch(WalletAction.CheckHashesCountOnline)
if (!store.state.walletState.updatingWallet) setupWalletUpdate(action.wallet)
tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.CreatePayId.CompleteCreatingPayId -> {
scope.launch {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey
if (cardId != null && wallet != null && publicKey != null) {
val result = PayIdManager().setPayId(
cardId, publicKey.toHexString(),
action.payId, wallet.address, wallet.blockchain
)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(WalletAction.CreatePayId.Success(action.payId))
is Result.Failure -> {
val error = result.error as? TapError
?: TapError.PayIdCreatingError
store.dispatch(WalletAction.CreatePayId.Failure(error))
}
}
}
}
}
}
is WalletAction.Scan -> {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.state.globalState.tapWalletManager
.onCardScanned(result.data, true)
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
store.dispatch(WalletAction.ScanCardFinished())
}
is CompletionResult.Failure -> {
if (result.error !is TangemSdkError.UserCancelled) {
// Weird things... If you run the code below without coroutines,
// then rescanning will be impossible
scope.launch(Dispatchers.Main) {
store.dispatch(WalletAction.ScanCardFinished(result.error))
if (store.state.walletState.scanCardFailsCounter >= 2) {
store.dispatch(WalletAction.ShowDialog.ScanFails)
}
}
}
}
}
}
}
is WalletAction.LoadData -> {
scope.launch {
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.tapWalletManager.loadData(it)
}
}
}
is NetworkStateChanged -> {
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.CheckHashesCountOnline)
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse)
}
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.walletAddresses?.selectedAddress?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.walletAddresses?.selectedAddress?.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount)
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.Warnings.CheckIfNeeded -> {
val globalState = store.state.globalState
val validator = globalState.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState.scanNoteResponse?.card?.let { card ->
store.state.globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning())
if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) }
}
updateWarningMessages()
}
}
is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.SaveCardId -> {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
is WalletAction.TwinsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
is WalletAction.Warnings.AppRating.RemindLater -> {
preferencesStorage.appRatingLaunchObserver.applyDelayedShowing()
}
is WalletAction.Warnings.AppRating.SetNeverToShow -> {
preferencesStorage.appRatingLaunchObserver.setNeverToShow()
}
}
next(action)
}
}
}
private fun tryToShowAppRatingWarning(wallet: Wallet) {
val nonZeroWalletsCount = wallet.amounts.filter {
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
}.size
if (nonZeroWalletsCount > 0) {
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
}
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISPLAYED)
addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
}
}
private fun setupWalletUpdate(wallet: Wallet) {
if (!wallet.recentTransactions.toPendingTransactions(wallet.address).isNullOrEmpty()) {
store.dispatch(WalletAction.UpdateWallet.ScheduleUpdatingWallet)
scope.launch(Dispatchers.IO) {
delay(10000)
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.UpdateWallet)
}
}
}
}
private fun prepareSendAction(amount: Amount?): Action {
return if (amount != null) {
if (amount.type is AmountType.Token) {
PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount)
} else {
PrepareSendScreen(amount)
}
} else {
val amounts = store.state.walletState.wallet?.amounts?.toSendableAmounts()
if (amounts?.size ?: 0 > 1) {
WalletAction.Send.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(amountToSend)
}
}
}
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null,
): WarningMessage? {
if (card.isTwinCard()) return null
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
WarningMessagesManager.alreadySignedHashesWarning()
} else {
store.dispatch(WalletAction.SaveCardId)
null
}
} else {
store.dispatch(WalletAction.NeedToCheckHashesCountOnline)
null
}
}
private fun checkHashesCountOnline() {
if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return
if (card.isTwinCard()) return
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
scope.launch {
val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
store.dispatch(WalletAction.ConfirmHashesCount)
store.dispatch(WalletAction.SaveCardId)
}
is SimpleResult.Failure ->
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
} else if (card.walletSignedHashes ?: 0 > 0) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
}
}
}
}
}
private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) {
store.state.globalState.warningManager?.addWarning(warning)
if (autoUpdate) updateWarningMessages()
}
private fun updateWarningMessages() {
val warningManager = store.state.globalState.warningManager ?: return
store.dispatch(WalletAction.Warnings.SetWarnings(
warningManager.getWarnings(WarningMessage.Location.MainScreen)))
}
}
private class TopUpMiddleware {
fun handle(action: WalletAction.TopUpAction) {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val config = store.state.globalState.configManager?.config ?: return
val addresses = store.state.walletState.walletAddresses ?: return
if (addresses.list.isEmpty()) return
val defaultAddress = addresses.list[0].address
val url = TopUpHelper.getUrl(
store.state.walletState.currencyData.currencySymbol!!,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
val customTabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(action.toolbarColor)
.build()
customTabsIntent.launchUrl(action.context, Uri.parse(url));
}
}
}
}

View file

@ -213,7 +213,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowQrCode -> {
is WalletAction.ShowDialog.QrCode -> {
val selectedWalletData = newState.getWalletData(newState.selectedWallet)
newState = newState.copy(
walletDialog = WalletDialog.QrDialog(
@ -223,6 +223,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
is WalletAction.ShowDialog.ScanFails -> {
newState = newState.copy(walletDialog = WalletDialog.ScanFailsDialog)
}
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}
@ -232,7 +235,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList)
is WalletAction.Warnings.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList)
is WalletAction.TopUpAction -> {
newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState))
}
is WalletAction.TopUpAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val selectedWalletData = newState.getWalletData(newState.selectedWallet)
@ -246,6 +252,13 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
newState = newState.copy(wallets = wallets)
}
is WalletAction.ScanCardFinished -> {
newState = if (action.scanError == null) {
newState.copy(scanCardFailsCounter = 0)
} else {
newState.copy(scanCardFailsCounter = newState.scanCardFailsCounter + 1)
}
}
}
return newState
}

View file

@ -25,6 +25,7 @@ data class WalletState(
val walletDialog: WalletDialog? = null,
val twinCardsState: TwinCardsState? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val scanCardFailsCounter: Int = 0,
val wallets: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = emptyList(),
val isMultiwalletAllowed: Boolean = false,
@ -101,6 +102,7 @@ sealed class WalletDialog {
) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
object ScanFailsDialog: WalletDialog()
}
enum class ProgressState { Loading, Done, Error }

View file

@ -19,6 +19,8 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
@ -26,6 +28,10 @@ import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog
import com.tangem.tap.features.wallet.ui.dialogs.QrDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
@ -160,6 +166,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
scanNoteResponse.card, scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed,
CardTou(),
store.state.globalState.appCurrency
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
@ -176,3 +183,78 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
//TODO: handle addition of testnets
//class MultipleAddressUiHelper {
// companion object {
// fun typeToId(type: AddressType): Int {
// return when (type) {
// is BitcoinAddressType.Legacy -> R.id.chip_legacy
// is BitcoinAddressType.Segwit -> R.id.chip_default
// is CardanoAddressType.Byron -> R.id.chip_legacy
// is CardanoAddressType.Shelley -> R.id.chip_default
// else -> View.NO_ID
// }
// }
//
// fun idToType(id: Int, blockchain: Blockchain?): AddressType? {
// return when (id) {
// R.id.chip_default -> {
// when (blockchain) {
// Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Segwit
// Blockchain.CardanoShelley -> CardanoAddressType.Shelley
// else -> null
// }
// }
// R.id.chip_legacy -> {
// when (blockchain) {
// Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Legacy
// Blockchain.CardanoShelley -> CardanoAddressType.Byron
// else -> null
// }
// }
// else -> null
// }
// }
// }
//}
//TODO: handle scan failed dialog:
//private fun handleDialogs(walletDialog: WalletDialog?) {
// when (walletDialog) {
// is WalletDialog.QrDialog -> {
// if (walletDialog.qrCode != null && walletDialog.shareUrl != null) {
// if (dialog == null) dialog = QrDialog(requireContext()).apply {
// this.showQr(
// walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName
// )
// }
// }
// }
// is WalletDialog.CreatePayIdDialog -> {
// when (walletDialog.creatingPayIdState) {
// CreatingPayIdState.EnterPayId -> {
// if (dialog == null) dialog = PayIdDialog(requireContext()).apply {
// this.show()
// }
// (dialog as? PayIdDialog)?.stopProgress()
// }
// CreatingPayIdState.Waiting -> (dialog as? PayIdDialog)?.showProgress()
// }
// }
// is WalletDialog.SelectAmountToSendDialog -> {
// if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply {
// this.show(walletDialog.amounts)
// }
// }
// is WalletDialog.ScanFailsDialog -> {
// if (dialog == null) dialog = ScanFailsDialog.create(requireContext()).apply {
// this.show()
// }
// }
// null -> {
// dialog?.dismiss()
// dialog = null
// }
// }
//}

View file

@ -8,10 +8,16 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.feedback.RateCanBeBetterEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.layout_warning.view.*
@ -40,7 +46,7 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(warning: WarningMessage) {
setBgColor(warning.priority)
setText(warning)
setupOkButton(warning)
setupControlButtons(warning)
}
private fun setText(warning: WarningMessage) {
@ -59,10 +65,50 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) {
view.card_view.setCardBackgroundColor(view.context.resources.getColor(color))
}
private fun setupOkButton(warning: WarningMessage) {
view.btn_got_it.show(warning.type == WarningMessage.Type.Temporary)
view.btn_got_it.setOnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
private fun setupControlButtons(warning: WarningMessage) {
when (warning.type) {
WarningMessage.Type.Permanent -> {
view.group_controls_temporary.hide()
view.group_controls_rating.hide()
}
WarningMessage.Type.Temporary -> {
view.group_controls_rating.hide()
view.group_controls_temporary.show()
view.btn_got_it.setOnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) }
}
WarningMessage.Type.AppRating -> {
view.group_controls_temporary.hide()
view.group_controls_rating.show()
view.btn_close.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
view.btn_can_be_better.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
}
view.btn_really_cool.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val context = view.context
val reviewManager = ReviewManagerFactory.create(context)
val flow = reviewManager.requestReviewFlow()
flow.addOnCompleteListener {
if (it.isSuccessful) {
// val info = it.result
// Toast.makeText(context, "success", Toast.LENGTH_SHORT).show()
} else {
// Toast.makeText(context, "fail", Toast.LENGTH_SHORT).show()
}
}.addOnFailureListener {
// Toast.makeText(context, "failure", Toast.LENGTH_SHORT).show()
}
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
}
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.feedback.ScanFailsEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class ScanFailsDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail()))
}
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
}.create()
}
}
}

View file

@ -11,16 +11,18 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.util.*
class PreferencesStorage(applicationContext: Application) {
private val preferences: SharedPreferences by lazy {
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
}
private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
val appRatingLaunchObserver: AppRatingLaunchObserver
init {
incrementLaunchCounter()
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
}
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
@ -97,4 +99,60 @@ class PreferencesStorage(applicationContext: Application) {
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
}
}
class AppRatingLaunchObserver(
private val preferences: SharedPreferences,
private val launchCounts: Int,
) {
private val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount"
private val K_FUNDS_FOUND_DATE = "fundsFoundDate"
private val K_USER_WAS_INTERACT_WITH_RATING = "userWasInteractWithRating"
private val deferShowing = 20
private val firstShowing = 3
private var fundsFoundDate: Calendar? = null
init {
val dateTimeMs = preferences.getLong(K_FUNDS_FOUND_DATE, -1)
if (dateTimeMs > 0) fundsFoundDate = Calendar.getInstance().apply { timeInMillis = dateTimeMs }
}
fun foundWalletWithFunds() {
if (fundsFoundDate != null) return
fundsFoundDate = Calendar.getInstance()
preferences.edit().putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply()
}
fun isReadyToShow(): Boolean {
val fundsDate = fundsFoundDate ?: return false
if (!userWasInteractWithRating()) {
val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis
val diffInDays = diff / (100 * 60 * 60 * 24)
if (diffInDays >= firstShowing) return true
}
val nextShowing = getCounterOfNextShowing()
return launchCounts >= nextShowing
}
fun applyDelayedShowing() {
updateNextShowing(launchCounts + deferShowing)
}
fun setNeverToShow() {
updateNextShowing(999999999)
}
private fun updateNextShowing(at: Int) {
val editor = preferences.edit()
editor.putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, at)
editor.putBoolean(K_USER_WAS_INTERACT_WITH_RATING, true)
editor.apply()
}
private fun userWasInteractWithRating(): Boolean = preferences.getBoolean(K_USER_WAS_INTERACT_WITH_RATING, false)
private fun getCounterOfNextShowing(): Int = preferences.getInt(K_SHOW_RATING_AT_LAUNCH_COUNT, firstShowing)
}

View file

@ -126,6 +126,21 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" />
<TextView
android:id="@+id/tv_card_tou"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="7dp"
android:paddingBottom="10dp"
android:drawablePadding="15dp"
android:textColor="@color/darkGray6"
android:textSize="16sp"
android:visibility="gone"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer"
android:text="@string/details_row_title_card_tou" />
<TextView
android:id="@+id/tv_settings_title"
android:layout_width="wrap_content"
@ -137,7 +152,7 @@
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer" />
app:layout_constraintTop_toBottomOf="@id/tv_card_tou" />
<View
android:layout_width="match_parent"
@ -170,6 +185,19 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_send_feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/details_row_title_send_feedback"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
<TextView
android:id="@+id/tv_card_title"
android:layout_width="wrap_content"
@ -181,7 +209,7 @@
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
<View
android:layout_width="match_parent"

View file

@ -20,14 +20,25 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginEnd="8dp"
android:text="Test title"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:tint="@android:color/white"
app:layout_constraintBottom_toBottomOf="@+id/tv_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tv_title"
app:srcCompat="@drawable/ic_close" />
<TextView
android:id="@+id/tv_message"
android:layout_width="0dp"
@ -54,9 +65,46 @@
android:text="@string/how_to_got_it_button"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<Button
android:id="@+id/btn_can_be_better"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/warning_button_can_be_better"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toStartOf="@+id/btn_really_cool"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<Button
android:id="@+id/btn_really_cool"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/warning_button_really_cool"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_temporary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="btn_got_it" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="btn_close, btn_can_be_better, btn_really_cool" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -12,7 +12,7 @@
<string name="common_camera_denied_alert_message">Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an</string>
<string name="common_camera_alert_button_settings">Einstellungen</string>
<string name="home_welcome">Willkommen bei Tangem. Haben Sie eine dieser Karten?</string>
<string name="home_welcome_back">Willkommen bei Tangem Tap. Scannen Sie Ihre Karten um zu starten.</string>
<string name="home_welcome_back">Willkommen bei Tangem. Scannen Sie Ihre Karten um zu starten.</string>
<string name="home_button_yes">Ja! Bitte scannen</string>
<string name="home_button_shop">Einkaufen</string>
<string name="home_ready_title">Halten Sie die \ncard hinter dem \nphone bereit und scannen Sie sie.</string>
@ -21,7 +21,7 @@
<string name="wallet_button_scan">Scannen</string>
<string name="wallet_button_send">Absenden</string>
<string name="wallet_button_create_wallet">ein Wallet erstellen</string>
<string name="wallet_title">Tangem Tap</string>
<string name="wallet_title">Tangem</string>
<string name="wallet_choice_wallet_option_title">eine Walletoption wählen</string>
<string name="wallet_balance_token_title">Token</string>
<string name="wallet_balance_tokens_title">Tokens</string>
@ -98,7 +98,7 @@
<string name="alert_card_signed_transactions">Diese Karte wurde früher bereits aufgeladen und Transaktionen wurden damit signiert. Ziehen Sie eine sofortige Auszahlung aller Beträge in Betracht, wenn Sie diese Karte von einer nicht vertrauenswürdigen Quelle erhalten haben.</string>
<string name="alert_title">Warnung</string>
<string name="alert_unsupported_card">Diese Karte ist für die Zusammenarbeit mit Tangem Tap nicht geeignet</string>
<string name="alert_unsupported_card">Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet</string>
<string name="alert_developer_card">Die von Ihnen gescannte Karte ist eine Entwicklungskarte. Akzeptieren Sie sie nicht als Zahlungsmittel</string>
<string name="alert_old_card">Die vor September 2019 ausgestellten Karten können mit iPhone momentan nicht extrahiert werden. Wir arbeiten eng mit Apple zusammen, damit es in den nächsten Versionen von iOS möglich wäre.</string>
<string name="initial_message_sign_header">Tippen um zu signieren</string>
@ -112,7 +112,7 @@
<string name="disclaimer_text">
RECHTLICHER HAFTUNGSAUSSCHLUSS
\n\n1.Tangem Tap App (Software)
\n\n1.Tangem App (Software)
 
\n\nDie Software ist nur für die Verwendung mit Tangem hardware Wallets (Karten) über NFC-Schnittstelle geeignet. Die Software:
 

View file

@ -13,7 +13,7 @@
<string name="common_camera_denied_alert_message">Vous n\'avez pas octroyé laccès à votre caméra, veuillez modifier vos paramètres de confidentialité </string>
<string name="common_camera_alert_button_settings">Paramètres </string>
<string name="home_welcome">Bienvenue à Tangem. Avez-vous une de nos cartes</string>
<string name="home_welcome_back"> Encore une fois, bienvenue dans Tangem Tap. \nScannez votre carte pour commencer.</string>
<string name="home_welcome_back"> Encore une fois, bienvenue dans Tangem. \nScannez votre carte pour commencer.</string>
<string name="home_button_yes">Oui! Scannez-la. </string>
<string name="home_button_shop">Magasin </string>
<string name="home_ready_title">Préparez-vous à appuyer votre carte au dos de votre téléphone.</string>
@ -22,7 +22,7 @@
<string name="wallet_button_scan">Scanner </string>
<string name="wallet_button_send">Envoyer </string>
<string name="wallet_button_create_wallet">Créer un portefeuille </string>
<string name="wallet_title">Tangem Tap</string>
<string name="wallet_title">Tangem</string>
<string name="wallet_choice_wallet_option_title">Choisir une variante de portefeuille </string>
<string name="wallet_balance_token_title"> Jeton d\'authentification </string>
<string name="wallet_balance_tokens_title">Jetons d\'authentification </string>
@ -98,7 +98,7 @@
<string name="alert_old_device_this_card">Vous pouvez rencontrer des problèmes NFC avec certains iPhone 7/7 + lors de lextraction </string>
<string name="alert_card_signed_transactions">Cette carte a déjà été rechargée et a signé des transactions avant. Envisagez la possibilité de retirer tous les fonds immédiatement si vous avez reçu cette carte d\'une source non fiable.</string>
<string name="alert_title">Attention</string>
<string name="alert_unsupported_card">Cette carte n\'est pas conçue pour fonctionner avec Tangem Tap </string>
<string name="alert_unsupported_card">Cette carte n\'est pas conçue pour fonctionner avec Tangem</string>
<string name="alert_developer_card">La carte que vous avez scannée est une carte de développement. Ne l\'acceptez pas comme paiement </string>
<string name="alert_old_card">Les cartes Tangem émises avant septembre 2019 ne peuvent actuellement pas être extraites à l\'aide de l\'iPhone. Nous travaillons activement avec Apple pour rendre cela possible dans les futures versions d\'iOS. </string>
<string name="initial_message_sign_header">Touchez pour signer </string>
@ -113,7 +113,7 @@
Avertissement
 
\n\n1.Application Tangem Tap (Logiciel)
\n\n1.Application Tangem (Logiciel)
 
\n\nLe logiciel est destiné à l\utilisation exclusivement avec les e-portefeuilles Tangem (Cartes) à l\aide de l\interface NFC. Le logiciel NE réalise pas ce qui suit :
 

View file

@ -11,7 +11,7 @@
<string name="common_camera_denied_alert_message">Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy</string>
<string name="common_camera_alert_button_settings">Impostazioni</string>
<string name="home_welcome">Benvenuti in Tangem. Hai una delle nostre schede?</string>>
<string name="home_welcome_back">Benevenuti di nuovo su Tangem Tap. Scansiona la tua carta per iniziare.</string>
<string name="home_welcome_back">Benevenuti di nuovo su Tangem. Scansiona la tua carta per iniziare.</string>
<string name="home_button_yes">Si! Scansiona carta.</string>
<string name="home_button_shop">Shop</string>
<string name="home_ready_title">Preparati ad avvicinare la tua carta sul retro del telefono.</string>
@ -20,7 +20,7 @@
<string name="wallet_button_scan">Scansiona</string>
<string name="wallet_button_send">Invia</string>
<string name="wallet_button_create_wallet">Crea portafoglio</string>
<string name="wallet_title">Tangem Tap</string>
<string name="wallet_title">Tangem</string>
<string name="wallet_choice_wallet_option_title">Scegli l\'opzione del portafoglio</string>
<string name="wallet_balance_token_title">Token</string>
<string name="wallet_balance_tokens_title">Token</string>
@ -95,7 +95,7 @@
<string name="alert_old_device_this_card">Potresti riscontrare problemi con l\'NFC su alcuni iPhone 7/7 + durante la rimozione</string>
<string name="alert_card_signed_transactions">Questa carta è già stata ricaricata e ha firmato transazioni in passato. Valuta la possibilità di prelevare immediatamente tutti i fondi se hai ricevuto questa carta da una fonte inaffidabile.</string>
<string name="alert_title">Attenzione</string>
<string name="alert_unsupported_card">Questa carta non è progettata per funzionare con Tangem Tap</string>
<string name="alert_unsupported_card">Questa carta non è progettata per funzionare con Tangem</string>
<string name="alert_developer_card">La carta che hai scansionato è una carta di sviluppo. Non utilizzarla come strumento di pagamento</string>
<string name="alert_old_card">Le schede emesse prima di settembre 2019 non possono attualmente essere utilizzate con un iPhone. Stiamo lavorando a stretto contatto con Apple per renderle disponibili nelle future versioni di iOS. </string>
<string name="initial_message_sign_header">Avvicina per firmare</string>
@ -110,7 +110,7 @@
NOTA LEGALE\n
\n
App Tangem Tap (software)\n
App Tangem (software)\n
\n
1. Il software può essere utilizzato solo con i portafogli hardware Tangem (schede) tramite l\'interfaccia NFC. Il software NON esegue le seguenti operazioni:\n
\n

View file

@ -1,6 +1,6 @@
<resources>
<string name="app_name" translatable="false">Tangem Tap</string>
<string name="app_name" translatable="false">Tangem</string>
<string name="common_no">No</string>
<string name="common_save_changes">Save changes</string>
@ -16,7 +16,7 @@
<string name="common_camera_alert_button_settings">Settings</string>
<string name="home_welcome">Welcome to Tangem.\nDo you have one of our cards?</string>
<string name="home_welcome_back">Welcome back to Tangem Tap. \nScan your card to start.</string>
<string name="home_welcome_back">Welcome back to Tangem. \nScan your card to start.</string>
<string name="home_button_shop">Shop</string>
<string name="home_ready_title">Get ready to tap your \ncard to the back of the \nphone.</string>
<string name="home_button_tapin">Tap in</string>
@ -26,7 +26,7 @@
<string name="wallet_button_scan">Scan</string>
<string name="wallet_button_send">Send</string>
<string name="wallet_button_create_wallet">Create wallet</string>
<string name="wallet_title">Tangem Tap</string>
<string name="wallet_title">Tangem</string>
<string name="wallet_choice_wallet_option_title">Choice wallet option</string>
<string name="wallet_balance_token_title">Token</string>
@ -109,7 +109,7 @@
<string name="alert_old_device_this_card">You may experience NFC problems with some iPhone 7/7+ during the extraction</string>
<string name="alert_card_signed_transactions">This card has been already topped up and signed transactions in the past. Consider immediate withdrawal of all funds if you have received this card from an untrusted source.</string>
<string name="alert_title">Warning</string>
<string name="alert_unsupported_card">This card it is not designed to work with Tangem Tap</string>
<string name="alert_unsupported_card">This card it is not designed to work with Tangem</string>
<string name="alert_developer_card">The card you scanned is a development card. Dont accept it as a payment</string>
<string name="alert_old_card">Tangem cards manufactured before September 2019 cannot currently be extracted with an iPhone. Were working hard with Apple to make it possible in future versions of iOS.</string>
@ -124,7 +124,7 @@
<string name="disclaimer_text">Legal Disclaimer
\n
\n1. Tangem Tap application (Software)
\n1. Tangem application (Software)
\n
\nThe Software is intended for usage only with Tangem hardware wallets (Cards) via NFC interface. The Software DOES NOT:
\n

View file

@ -27,6 +27,7 @@ this wallet.
<string name="details_twins_recreate_subtitle" translatable="false">The wallet creation procedure consists of three steps. You must complete it to the end, otherwise you will have to start from the beginning.</string>
<string name="details_twins_recreate_button_format" translatable="false">Tap the card #%s</string>
<string name="details_twins_recreate_alert" translatable="false">You have already started the process of recreating twin wallet. If you interrupt it, you won\'t be able to use your twin cards until you start it again and complete recreating the wallet.</string>
<string name="details_row_title_card_tou" translatable="false">Card terms of use</string>
<string name="notification_twins_recreate_success" translatable="false">The twin address was successfully created</string>
@ -43,6 +44,25 @@ this wallet.
<string name="send_error_invalid_destination_tag" translatable="false">Invalid destination tag. It won\'t be added to the transaction</string>
<string name="send_error_invalid_memo_id" translatable="false">Invalid Memo ID. It won\'t be added to the transaction</string>
<string name="details_section_title_app" translatable="false">App</string>
<string name="details_row_title_send_feedback" translatable="false">Send feedback</string>
<string name="alert_app_feedback_sent_title" translatable="false">Sent successfully</string>
<string name="alert_app_feedback_sent_message" translatable="false">Thank you for your feedback</string>
<string name="alert_negative_app_rate_sent_title" translatable="false">Your suggestions were sent</string>
<string name="alert_negative_app_rate_sent_message" translatable="false">Thank your for your feedback. We will response as soon as possible</string>
<string name="alert_failed_to_send_email_title" translatable="false">Failed to send email</string>
<string name="alert_failed_to_send_email_message" translatable="false">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title" translatable="false">Cant send a transaction</string>
<string name="alert_failed_to_send_transaction_message" translatable="false">Reason: %s. Do you want to send feedback?</string>
<string name="alert_troubleshooting_scan_card_title" translatable="false">Are you having difficulty scanning your card?</string>
<string name="alert_troubleshooting_scan_card_message" translatable="false">Please try to tap the card exactly as shown in the animation or request support.</string>
<string name="alert_button_try_again" translatable="false">Try again</string>
<string name="alert_button_request_support" translatable="false">Request support</string>
<string name="alert_button_send_feedback" translatable="false">Send feedback</string>
<string name="warning_button_really_cool" translatable="false">Really cool!</string>
<string name="warning_button_can_be_better" translatable="false">Can be better</string>
<string name="warning_rate_app_title" translatable="false">One question</string>
<string name="warning_rate_app_message" translatable="false">How do you like Tangem?</string>
<string name="custom_token_creation_error_empty_field" translatable="false">All fields are required</string>
@ -70,7 +90,7 @@ this wallet.
//Details
<string name="details_section_title_blockchain" translatable="false">Blockchain</string>
<string name="details_row_title_manage_tokens" translatable="false">Manage tokens</string>
<string name="add_tokens_subtitle_blockchains" translatable="false">Blockchains</string>
<string name="add_tokens_subtitle_tokens" translatable="false">Ethereum Tokens</string>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="files" path="/"/>
</paths>