Updated on 2026-08-14
This commit is contained in:
commit
48718fa782
77 changed files with 1511 additions and 1076 deletions
|
|
@ -93,10 +93,9 @@ dependencies {
|
|||
implementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'com.tangem:blockchain:develop-93'
|
||||
// implementation 'com.tangem:blockchain:0.0.1'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154'
|
||||
implementation "com.tangem:blockchain:${versions.tamgem_blockchain_sdk}"
|
||||
implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tamgem_card_sdk}"
|
||||
implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tamgem_card_sdk}"
|
||||
|
||||
// WebView
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
|
|
@ -145,8 +144,11 @@ dependencies {
|
|||
implementation("com.squareup.okhttp3:okhttp")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
//
|
||||
// //Crypto
|
||||
// Support
|
||||
implementation("com.zendesk:support:5.0.9")
|
||||
implementation("com.zendesk:messaging:5.2.4")
|
||||
|
||||
//Crypto
|
||||
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
||||
|
||||
// animation
|
||||
|
|
|
|||
|
|
@ -128,6 +128,21 @@
|
|||
|
||||
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
|
||||
|
||||
<activity android:name="zendesk.messaging.MessagingActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.guide.HelpCenterActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.guide.ViewArticleActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.request.RequestActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.requestlist.RequestListActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
@ -140,4 +155,4 @@
|
|||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f50133547b5e19fb7ad32e64370573d60f6791fd
|
||||
Subproject commit 59ee0bff06ee54f3202421c28f7fb6e3d421dad8
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.os.Bundle
|
||||
import java.util.WeakHashMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ForegroundActivityObserver {
|
||||
private val activities = WeakHashMap<KClass<out Activity>, Activity>()
|
||||
|
||||
val foregroundActivity: Activity?
|
||||
get() = activities.entries
|
||||
.filterNot { it.value.isDestroyed }
|
||||
.firstOrNull()
|
||||
?.value
|
||||
|
||||
val callbacks get() = Callbacks()
|
||||
|
||||
inner class Callbacks : ActivityLifecycleCallbacks {
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
activities[activity::class] = activity
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activities.remove(activity::class)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
}
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
}
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
}
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ForegroundActivityObserver.withForegroundActivity(
|
||||
block: (Activity) -> Unit
|
||||
) {
|
||||
foregroundActivity?.let { block(it) }
|
||||
}
|
||||
|
|
@ -27,11 +27,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
|
||||
|
|
@ -57,7 +57,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
systemActions()
|
||||
store.state.globalState.feedbackManager?.updateActivity(this)
|
||||
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
|
||||
|
||||
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
|||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
|
||||
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.appReducer
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -24,9 +27,6 @@ import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
|
|||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
import com.tangem.tap.features.feedback.AdditionalEmailInfo
|
||||
import com.tangem.tap.features.feedback.FeedbackManager
|
||||
import com.tangem.tap.features.feedback.TangemLogCollector
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.wallet.BuildConfig
|
||||
|
|
@ -40,6 +40,8 @@ val store = Store(
|
|||
)
|
||||
val logConfig = LogConfig()
|
||||
|
||||
lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
||||
|
||||
lateinit var preferencesStorage: PreferencesStorage
|
||||
lateinit var currenciesRepository: CurrenciesRepository
|
||||
lateinit var walletConnectRepository: WalletConnectRepository
|
||||
|
|
@ -68,6 +70,8 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
)
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
foregroundActivityObserver = ForegroundActivityObserver()
|
||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
initFeedbackManager()
|
||||
loadConfigs()
|
||||
|
||||
|
|
@ -85,16 +89,23 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
val localLoader = FeaturesLocalLoader(this, moshi)
|
||||
val remoteLoader = FeaturesRemoteLoader(moshi)
|
||||
val configManager = ConfigManager(localLoader, remoteLoader)
|
||||
configManager.load {
|
||||
configManager.load { config ->
|
||||
store.dispatch(GlobalAction.SetConfigManager(configManager))
|
||||
shopService = TangemShopService(this, configManager.config.shopify!!)
|
||||
shopService = TangemShopService(
|
||||
application = this,
|
||||
shopifyShop = config.shopify!!
|
||||
)
|
||||
store.state.globalState.feedbackManager?.initChat(
|
||||
context = this,
|
||||
zendeskConfig = config.zendesk!!
|
||||
)
|
||||
}
|
||||
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
|
||||
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
|
||||
}
|
||||
|
||||
private fun initFeedbackManager() {
|
||||
val infoHolder = AdditionalEmailInfo()
|
||||
val infoHolder = AdditionalFeedbackInfo()
|
||||
infoHolder.setAppVersion(this)
|
||||
|
||||
val logWriter = TangemLogCollector()
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
primaryButtonRes = state.dialog.primaryButtonRes,
|
||||
primaryButtonAction = state.dialog.onOk
|
||||
)
|
||||
is WalletDialog.RussianCardholdersWarningDialog ->
|
||||
RussianCardholdersWarningBottomSheetDialog(context)
|
||||
is DetailsDialog.ConfirmDisablingSaving -> {
|
||||
val messageRes = when (state.dialog.setting) {
|
||||
PrivacySetting.SAVE_CARDS -> R.string.security_and_privacy_save_card_disable_warning
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
|||
|
||||
fun WalletManager?.getToUpUrl(): String? {
|
||||
val globalState = store.state.globalState
|
||||
val currencyExchangeManager = globalState.currencyExchangeManager ?: return null
|
||||
val exchangeManager = globalState.exchangeManager ?: return null
|
||||
val wallet = this?.wallet ?: return null
|
||||
|
||||
val defaultAddress = wallet.address
|
||||
return currencyExchangeManager.getUrl(
|
||||
return exchangeManager.getUrl(
|
||||
action = CurrencyExchangeManager.Action.Buy,
|
||||
blockchain = wallet.blockchain,
|
||||
cryptoCurrencyName = wallet.blockchain.currency,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
|
||||
class AdditionalFeedbackInfo {
|
||||
class EmailWalletInfo(
|
||||
var blockchain: Blockchain = Blockchain.Unknown,
|
||||
var address: String = "",
|
||||
var explorerLink: String = "",
|
||||
var host: String = "",
|
||||
var derivationPath: String = "",
|
||||
)
|
||||
|
||||
var appVersion: String = ""
|
||||
|
||||
// card
|
||||
var cardId: String = ""
|
||||
var cardFirmwareVersion: String = ""
|
||||
var cardIssuer: String = ""
|
||||
var cardBlockchain: String = ""
|
||||
|
||||
// wallets
|
||||
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
|
||||
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
|
||||
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
|
||||
var signedHashesCount: String = ""
|
||||
|
||||
// device
|
||||
var phoneModel: String = Build.MODEL
|
||||
var osVersion: String = Build.VERSION.SDK_INT.toString()
|
||||
|
||||
// send error
|
||||
var destinationAddress: String = ""
|
||||
var amount: String = ""
|
||||
var fee: String = ""
|
||||
var token: String = ""
|
||||
|
||||
fun setAppVersion(context: Context) {
|
||||
try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
appVersion = pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCardInfo(data: ScanResponse) {
|
||||
cardId = data.card.cardId
|
||||
cardBlockchain = data.walletData?.blockchain ?: ""
|
||||
cardFirmwareVersion = data.card.firmwareVersion.stringValue
|
||||
cardIssuer = data.card.issuer.name
|
||||
signedHashesCount = data.card.wallets
|
||||
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
}
|
||||
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
walletsInfo.clear()
|
||||
tokens.clear()
|
||||
walletManagers.forEach { manager ->
|
||||
walletsInfo.add(
|
||||
EmailWalletInfo(
|
||||
blockchain = manager.wallet.blockchain,
|
||||
address = getAddress(manager.wallet),
|
||||
explorerLink = getExploreUri(manager.wallet),
|
||||
host = manager.currentHost,
|
||||
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
)
|
||||
if (manager.cardTokens.isNotEmpty()) {
|
||||
tokens[manager.wallet.blockchain] = manager.cardTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateOnSendError(
|
||||
wallet: Wallet,
|
||||
host: String,
|
||||
amountToSend: Amount,
|
||||
feeAmount: Amount,
|
||||
destinationAddress: String,
|
||||
) {
|
||||
onSendErrorWalletInfo = EmailWalletInfo(
|
||||
blockchain = wallet.blockchain,
|
||||
address = getAddress(wallet),
|
||||
explorerLink = getExploreUri(wallet),
|
||||
host = host,
|
||||
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
|
||||
this.destinationAddress = destinationAddress
|
||||
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
|
||||
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
|
||||
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
|
||||
}
|
||||
|
||||
private fun getAddress(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.address
|
||||
} else {
|
||||
val addresses = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${it.value}"
|
||||
}
|
||||
"Multiple address: $addresses"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExploreUri(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.getExploreUrl(wallet.address)
|
||||
} else {
|
||||
val links = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
|
||||
}
|
||||
"Multiple explorers links: $links"
|
||||
}
|
||||
}
|
||||
}
|
||||
108
app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
Normal file
108
app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.domain.common.TapWorkarounds
|
||||
import com.tangem.wallet.R
|
||||
|
||||
interface FeedbackData {
|
||||
val subjectResId: Int
|
||||
val mainMessageResId: Int
|
||||
|
||||
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
|
||||
|
||||
fun prepare(infoHolder: AdditionalFeedbackInfo) {}
|
||||
|
||||
fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String
|
||||
|
||||
fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String {
|
||||
return StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(3)
|
||||
append(context.getString(getDataCollectionMessageResId()))
|
||||
appendLine()
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class RateCanBeBetterEmail : FeedbackData {
|
||||
override val subjectResId: Int = R.string.feedback_subject_rate_negative
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class ScanFailsEmail : FeedbackData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_scan_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
|
||||
|
||||
override fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String = StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(4)
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class SendTransactionFailedEmail(
|
||||
val error: String
|
||||
) : FeedbackData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_tx_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendDelimiter()
|
||||
.appendTxFailedBlockchainInfo(error)
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class FeedbackEmail : FeedbackData {
|
||||
override val subjectResId: Int
|
||||
get() = if (isS2CCard) s2cSubject else tangemSubject
|
||||
override val mainMessageResId: Int
|
||||
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
|
||||
|
||||
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
|
||||
private val tangemMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private val s2cSubject: Int = R.string.feedback_subject_support
|
||||
private val s2cMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private var isS2CCard = false
|
||||
|
||||
override fun prepare(infoHolder: AdditionalFeedbackInfo) {
|
||||
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
|
||||
}
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendWalletsInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class SupportInfo : FeedbackData {
|
||||
override val subjectResId: Int = R.string.details_ask_a_question
|
||||
override val mainMessageResId: Int = R.string.details_ask_a_question
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String {
|
||||
return FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendDelimiter()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
class FeedbackDataBuilder(
|
||||
private val infoHolder: AdditionalFeedbackInfo
|
||||
) {
|
||||
val builder = StringBuilder()
|
||||
|
||||
fun appendDelimiter(): FeedbackDataBuilder {
|
||||
builder.appendDelimiter()
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendLine(count: Int = 1): FeedbackDataBuilder {
|
||||
builder.appendLine(count)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendCardInfo(): FeedbackDataBuilder {
|
||||
builder.appendKeyValue("Card ID", infoHolder.cardId)
|
||||
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
|
||||
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
|
||||
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendWalletsInfo(): FeedbackDataBuilder {
|
||||
infoHolder.walletsInfo.forEach {
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
|
||||
builder.appendKeyValue("Host", it.host)
|
||||
builder.appendKeyValue("Wallet address", it.address)
|
||||
builder.appendKeyValue("Derivation path", it.derivationPath)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
|
||||
infoHolder.tokens[it.blockchain]?.let { tokens ->
|
||||
builder.append("Tokens:")
|
||||
appendLine()
|
||||
tokens.forEach { token ->
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
builder.appendKeyValue("ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Contract address", token.contractAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendTxFailedBlockchainInfo(error: String): FeedbackDataBuilder {
|
||||
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalFeedbackInfo.EmailWalletInfo()
|
||||
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
|
||||
builder.appendKeyValue("Host", walletInfo.host)
|
||||
builder.appendKeyValue("Token", infoHolder.token)
|
||||
builder.appendKeyValue("Error", error)
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Source address", walletInfo.address)
|
||||
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
|
||||
builder.appendKeyValue("Amount", infoHolder.amount)
|
||||
builder.appendKeyValue("Fee", infoHolder.fee)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendPhoneInfo(): FeedbackDataBuilder {
|
||||
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
builder.appendKeyValue("OS version", infoHolder.osVersion)
|
||||
builder.appendKeyValue("App version", infoHolder.appVersion)
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String = builder.toString()
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
|
||||
return if (value.isNotBlank()) this.append("$key: $value\n") else this
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
|
||||
|
||||
private fun StringBuilder.appendLine(count: Int = 1): StringBuilder {
|
||||
return append(List(count) { "\n" }.joinToString(separator = ""))
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.domain.common.TapWorkarounds
|
||||
import com.tangem.tap.common.extensions.sendEmail
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.StringWriter
|
||||
import timber.log.Timber
|
||||
import zendesk.configurations.Configuration
|
||||
import zendesk.core.AnonymousIdentity
|
||||
import zendesk.core.Zendesk
|
||||
import zendesk.support.Support
|
||||
import zendesk.support.request.RequestConfiguration
|
||||
import zendesk.support.requestlist.RequestListActivity
|
||||
import zendesk.support.requestlist.RequestListConfiguration
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeedbackManager(
|
||||
val infoHolder: AdditionalFeedbackInfo,
|
||||
private val logCollector: TangemLogCollector,
|
||||
) {
|
||||
fun initChat(
|
||||
context: Context,
|
||||
zendeskConfig: ZendeskConfig,
|
||||
) {
|
||||
Zendesk.INSTANCE.init(
|
||||
/* context = */ context,
|
||||
/* zendeskUrl = */ zendeskConfig.url,
|
||||
/* applicationId = */ zendeskConfig.appId,
|
||||
/* oauthClientId = */ zendeskConfig.clientId,
|
||||
)
|
||||
Support.INSTANCE.init(Zendesk.INSTANCE)
|
||||
Zendesk.INSTANCE.setIdentity(AnonymousIdentity())
|
||||
}
|
||||
|
||||
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
|
||||
feedbackData.prepare(infoHolder)
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
val fileLog = if (feedbackData is ScanFailsEmail) createLogFile(activity) else null
|
||||
activity.sendEmail(
|
||||
email = getSupportEmail(),
|
||||
subject = activity.getString(feedbackData.subjectResId),
|
||||
message = feedbackData.joinTogether(activity, infoHolder),
|
||||
file = fileLog,
|
||||
onFail = onFail
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openChat(feedbackData: FeedbackData) {
|
||||
feedbackData.prepare(infoHolder)
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
RequestListActivity.builder()
|
||||
.show(
|
||||
/* context = */ activity,
|
||||
/* configurations = */ buildConfigs(activity, feedbackData)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLogFile(context: Context): File? {
|
||||
return try {
|
||||
val file = File(context.filesDir, "logs.txt")
|
||||
file.delete()
|
||||
file.createNewFile()
|
||||
|
||||
val stringWriter = StringWriter()
|
||||
logCollector.getLogs().forEach { stringWriter.append(it) }
|
||||
val fileWriter = FileWriter(file)
|
||||
fileWriter.write(stringWriter.toString())
|
||||
fileWriter.close()
|
||||
logCollector.clearLogs()
|
||||
file
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Can't create the logs file")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildConfigs(
|
||||
context: Context,
|
||||
feedbackData: FeedbackData,
|
||||
): List<Configuration> {
|
||||
return listOf(
|
||||
// Request configuration
|
||||
RequestConfiguration.Builder()
|
||||
.withRequestSubject(context.getString(feedbackData.subjectResId))
|
||||
.config(),
|
||||
// Request list configuration
|
||||
RequestListConfiguration.Builder()
|
||||
.withContactUsButtonVisible(true)
|
||||
.config(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportEmail(): String {
|
||||
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
|
||||
S2C_SUPPORT_EMAIL
|
||||
} else {
|
||||
DEFAULT_SUPPORT_EMAIL
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
|
||||
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.tap.common.log
|
||||
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdkLogger
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
class TangemLogCollector : TangemSdkLogger {
|
||||
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
|
||||
private val logs = mutableListOf<String>()
|
||||
private val mutex = Object()
|
||||
|
||||
override fun log(message: () -> String, level: Log.Level) {
|
||||
val time = dateFormatter.format(Date())
|
||||
synchronized(mutex) {
|
||||
logs.add("$time: ${message()}\n")
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
|
||||
|
||||
fun clearLogs() {
|
||||
synchronized(mutex) { logs.clear() }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,15 +7,18 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.redux.*
|
||||
import com.tangem.tap.common.feedback.FeedbackData
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.redux.DebugErrorAction
|
||||
import com.tangem.tap.common.redux.ErrorAction
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.ToastNotificationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.tap.features.feedback.EmailData
|
||||
import com.tangem.tap.features.feedback.FeedbackManager
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class GlobalAction : Action {
|
||||
|
|
@ -74,10 +77,21 @@ sealed class GlobalAction : Action {
|
|||
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
|
||||
data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction()
|
||||
|
||||
data class SendFeedback(val emailData: EmailData) : GlobalAction()
|
||||
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class OpenChat(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
|
||||
|
||||
object InitCurrencyExchangeManager : GlobalAction() {
|
||||
data class Success(val exchangeManager: CurrencyExchangeManager) : GlobalAction()
|
||||
object ExchangeManager : GlobalAction() {
|
||||
object Init : GlobalAction() {
|
||||
data class Success(
|
||||
val exchangeManager: com.tangem.tap.network.exchangeServices.CurrencyExchangeManager,
|
||||
) : GlobalAction()
|
||||
}
|
||||
|
||||
object Update : GlobalAction()
|
||||
}
|
||||
|
||||
object FetchUserCountry : GlobalAction() {
|
||||
data class Success(val countryCode: String) : GlobalAction()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,20 +2,31 @@ package com.tangem.tap.common.redux.global
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.currenciesRepository
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi
|
||||
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
|
||||
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
|
||||
import com.tangem.tap.network.exchangeServices.onramper.OnramperService
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class GlobalMiddleware {
|
||||
|
|
@ -24,94 +35,134 @@ class GlobalMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private val globalMiddlewareHandler: Middleware<AppState> = { _, appState ->
|
||||
private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
|
||||
when (action.result) {
|
||||
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
handleAction(action, appState, dispatch)
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(action: Action, appState: () -> AppState?, dispatch: DispatchFunction) {
|
||||
when (action) {
|
||||
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
|
||||
when (action.result) {
|
||||
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
is CompletionResult.Failure -> {
|
||||
if (action.result.error is TangemSdkError.UserCancelled) {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
|
||||
if (store.state.globalState.scanCardFailsCounter >= 2) {
|
||||
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
|
||||
}
|
||||
} else {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is GlobalAction.RestoreAppCurrency -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
|
||||
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
|
||||
))
|
||||
}
|
||||
is GlobalAction.HideWarningMessage -> {
|
||||
store.state.globalState.warningManager?.let {
|
||||
if (it.hideWarning(action.warning)) {
|
||||
if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) {
|
||||
//TODO: No appropriate warningMessage identification. Make it better later
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
|
||||
}
|
||||
|
||||
store.dispatch(WalletAction.Warnings.Update)
|
||||
store.dispatch(SendAction.Warnings.Update)
|
||||
}
|
||||
}
|
||||
}
|
||||
is GlobalAction.SendEmail -> {
|
||||
store.state.globalState.feedbackManager?.sendEmail(action.feedbackData)
|
||||
}
|
||||
is GlobalAction.OpenChat -> {
|
||||
store.state.globalState.feedbackManager?.openChat(action.feedbackData)
|
||||
}
|
||||
is GlobalAction.UpdateWalletSignedHashes -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
|
||||
}
|
||||
is GlobalAction.UpdateFeedbackInfo -> {
|
||||
store.state.globalState.feedbackManager?.infoHolder
|
||||
?.setWalletsInfo(action.walletManagers)
|
||||
}
|
||||
is GlobalAction.ExchangeManager.Init -> {
|
||||
val config = appState()?.globalState?.configManager?.config
|
||||
ifNotNull(
|
||||
config?.mercuryoWidgetId,
|
||||
config?.mercuryoSecret,
|
||||
config?.moonPayApiKey,
|
||||
config?.moonPayApiSecretKey,
|
||||
) { mercuryoWidgetId, mercuryoSecret, moonPayKey, moonPaySecretKey ->
|
||||
scope.launch {
|
||||
val buyService = MercuryoService(
|
||||
apiVersion = MercuryoApi.API_VERSION,
|
||||
mercuryoWidgetId = mercuryoWidgetId,
|
||||
secret = mercuryoSecret,
|
||||
)
|
||||
val sellService = MoonPayService(moonPayKey, moonPaySecretKey)
|
||||
val exchangeManager = CurrencyExchangeManager(buyService, sellService)
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
|
||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
|
||||
}
|
||||
}
|
||||
}
|
||||
is GlobalAction.ExchangeManager.Init.Success -> {}
|
||||
is GlobalAction.ExchangeManager.Update -> {
|
||||
val exchangeManager = appState()?.globalState?.exchangeManager.guard {
|
||||
store.dispatchDebugErrorNotification("exchangeManager is not initialized")
|
||||
return
|
||||
}
|
||||
scope.launch { exchangeManager.update() }
|
||||
}
|
||||
is GlobalAction.ScanCard -> {
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.scanProduct(
|
||||
store.state.globalState.analyticsHandlers,
|
||||
currenciesRepository,
|
||||
action.additionalBlockchainsToDerive,
|
||||
action.messageResId
|
||||
)
|
||||
withMainContext {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
|
||||
action.onSuccess?.invoke(result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
if (action.result.error is TangemSdkError.UserCancelled) {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
|
||||
if (store.state.globalState.scanCardFailsCounter >= 2) {
|
||||
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
|
||||
}
|
||||
} else {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is GlobalAction.RestoreAppCurrency -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
|
||||
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
|
||||
))
|
||||
}
|
||||
is GlobalAction.HideWarningMessage -> {
|
||||
store.state.globalState.warningManager?.let {
|
||||
if (it.hideWarning(action.warning)) {
|
||||
if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) {
|
||||
//TODO: No appropriate warningMessage identification. Make it better later
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
|
||||
}
|
||||
|
||||
store.dispatch(WalletAction.Warnings.Update)
|
||||
store.dispatch(SendAction.Warnings.Update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is GlobalAction.SendFeedback -> {
|
||||
store.state.globalState.feedbackManager?.send(action.emailData)
|
||||
}
|
||||
is GlobalAction.UpdateWalletSignedHashes -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
|
||||
}
|
||||
is GlobalAction.UpdateFeedbackInfo -> {
|
||||
store.state.globalState.feedbackManager?.infoHolder
|
||||
?.setWalletsInfo(action.walletManagers)
|
||||
}
|
||||
is GlobalAction.InitCurrencyExchangeManager -> {
|
||||
val config = appState()?.globalState?.configManager?.config
|
||||
ifNotNull(
|
||||
config?.onramperApiKey,
|
||||
config?.moonPayApiKey,
|
||||
config?.moonPayApiSecretKey,
|
||||
) { onramperKey, moonPayKey, moonPaySecretKey ->
|
||||
scope.launch {
|
||||
val onramper = OnramperService(onramperKey)
|
||||
val moonPay = MoonPayService(moonPayKey, moonPaySecretKey)
|
||||
val exchangeManager = CurrencyExchangeManager(onramper, moonPay)
|
||||
exchangeManager.getStatus()
|
||||
store.dispatchOnMain(GlobalAction.InitCurrencyExchangeManager.Success(exchangeManager))
|
||||
}
|
||||
}
|
||||
}
|
||||
is GlobalAction.ScanCard -> {
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.scanProduct(
|
||||
store.state.globalState.analyticsHandlers,
|
||||
currenciesRepository,
|
||||
action.additionalBlockchainsToDerive,
|
||||
action.messageResId
|
||||
)
|
||||
withMainContext {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
|
||||
action.onSuccess?.invoke(result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
action.onFailure?.invoke(result.error)
|
||||
}
|
||||
}
|
||||
action.onFailure?.invoke(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nextDispatch(action)
|
||||
}
|
||||
is GlobalAction.FetchUserCountry -> {
|
||||
scope.launch {
|
||||
val techService = store.state.domainNetworks.tangemTechService
|
||||
when (val result = techService.userCountry()) {
|
||||
is Result.Success -> {
|
||||
store.dispatch(
|
||||
GlobalAction.FetchUserCountry.Success(
|
||||
countryCode = result.data.code.lowercase()
|
||||
)
|
||||
)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
store.dispatch(
|
||||
GlobalAction.FetchUserCountry.Success(
|
||||
countryCode = Locale.getDefault().country.lowercase()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,12 +70,14 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
is GlobalAction.HideDialog -> {
|
||||
globalState.copy(dialog = null)
|
||||
}
|
||||
is GlobalAction.InitCurrencyExchangeManager.Success -> {
|
||||
globalState.copy(currencyExchangeManager = action.exchangeManager)
|
||||
is GlobalAction.ExchangeManager.Init.Success -> {
|
||||
globalState.copy(exchangeManager = action.exchangeManager)
|
||||
}
|
||||
is GlobalAction.SetIfCardVerifiedOnline ->
|
||||
globalState.copy(cardVerifiedOnline = action.verified)
|
||||
|
||||
is GlobalAction.FetchUserCountry.Success -> globalState.copy(
|
||||
userCountryCode = action.countryCode
|
||||
)
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,12 @@ package com.tangem.tap.common.redux.global
|
|||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.analytics.AnalyticsHandler
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.domain.PayIdManager
|
||||
import com.tangem.tap.domain.TapWalletManager
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.feedback.FeedbackManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import org.rekotlin.StateType
|
||||
|
|
@ -25,9 +25,10 @@ data class GlobalState(
|
|||
val appCurrency: FiatCurrency = FiatCurrency.Default,
|
||||
val scanCardFailsCounter: Int = 0,
|
||||
val dialog: StateDialog? = null,
|
||||
val currencyExchangeManager: CurrencyExchangeManager? = null,
|
||||
val exchangeManager: CurrencyExchangeManager? = null,
|
||||
val resources: AndroidResources = AndroidResources(),
|
||||
val analyticsHandlers: AnalyticsHandler? = null,
|
||||
val userCountryCode: String? = null,
|
||||
) : StateType
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.tap.common.zendesk
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ZendeskConfig(
|
||||
@Json(name = "zendeskApiKey")
|
||||
val apiKey: String,
|
||||
@Json(name = "zendeskAppId")
|
||||
val appId: String,
|
||||
@Json(name = "zendeskClientId")
|
||||
val clientId: String,
|
||||
@Json(name = "zendeskUrl")
|
||||
val url: String,
|
||||
)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.tap.domain.configurable.config
|
||||
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.shop.shopify.ShopifyShop
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.domain.configurable.Loader
|
||||
|
||||
/**
|
||||
|
|
@ -11,15 +11,17 @@ import com.tangem.tap.domain.configurable.Loader
|
|||
data class Config(
|
||||
val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de",
|
||||
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
|
||||
val onramperApiKey: String = "pk_test_Ix2aCF3ej_5tcDKkBR7MChIvf5Nb0oPORPQ3Oal5G8I0",
|
||||
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
|
||||
val mercuryoWidgetId: String = "",
|
||||
val mercuryoSecret: String = "",
|
||||
val appsFlyerDevKey: String = "",
|
||||
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
|
||||
val isSendingToPayIdEnabled: Boolean = true,
|
||||
val isTopUpEnabled: Boolean = false,
|
||||
@Deprecated("Not relevant since version 3.23")
|
||||
val isCreatingTwinCardsAllowed: Boolean = false,
|
||||
val shopify: ShopifyShop? = null
|
||||
val shopify: ShopifyShop? = null,
|
||||
val zendesk: ZendeskConfig? = null,
|
||||
)
|
||||
|
||||
class ConfigManager(
|
||||
|
|
@ -32,11 +34,11 @@ class ConfigManager(
|
|||
|
||||
private var defaultConfig = Config()
|
||||
|
||||
fun load(onComplete: VoidCallback? = null) {
|
||||
localLoader.load { config ->
|
||||
setupFeature(config.features)
|
||||
setupKey(config.configValues)
|
||||
onComplete?.invoke()
|
||||
fun load(onComplete: ((config: Config) -> Unit)? = null) {
|
||||
localLoader.load { configModel ->
|
||||
setupFeature(configModel.features)
|
||||
setupKey(configModel.configValues)
|
||||
onComplete?.invoke(config)
|
||||
}
|
||||
// Uncomment to enable remote config
|
||||
// remoteLoader.load { config ->
|
||||
|
|
@ -82,8 +84,9 @@ class ConfigManager(
|
|||
config = config.copy(
|
||||
coinMarketCapKey = values.coinMarketCapKey,
|
||||
moonPayApiKey = values.moonPayApiKey,
|
||||
onramperApiKey = values.onramperApiKey,
|
||||
moonPayApiSecretKey = values.moonPayApiSecretKey,
|
||||
mercuryoWidgetId = values.mercuryoWidgetId,
|
||||
mercuryoSecret = values.mercuryoSecret,
|
||||
blockchainSdkConfig = BlockchainSdkConfig(
|
||||
blockchairApiKey = values.blockchairApiKey,
|
||||
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
|
||||
|
|
@ -92,12 +95,14 @@ class ConfigManager(
|
|||
),
|
||||
appsFlyerDevKey = values.appsFlyerDevKey,
|
||||
shopify = values.shopifyShop,
|
||||
zendesk = values.zendesk,
|
||||
)
|
||||
defaultConfig = defaultConfig.copy(
|
||||
coinMarketCapKey = values.coinMarketCapKey,
|
||||
moonPayApiKey = values.moonPayApiKey,
|
||||
onramperApiKey = values.onramperApiKey,
|
||||
moonPayApiSecretKey = values.moonPayApiSecretKey,
|
||||
mercuryoWidgetId = values.mercuryoWidgetId,
|
||||
mercuryoSecret = values.mercuryoSecret,
|
||||
blockchainSdkConfig = BlockchainSdkConfig(
|
||||
blockchairApiKey = values.blockchairApiKey,
|
||||
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
|
||||
|
|
@ -106,6 +111,7 @@ class ConfigManager(
|
|||
),
|
||||
appsFlyerDevKey = values.appsFlyerDevKey,
|
||||
shopify = values.shopifyShop,
|
||||
zendesk = values.zendesk,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.domain.configurable.config
|
||||
|
||||
import com.tangem.tap.common.shop.shopify.ShopifyShop
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -15,15 +16,17 @@ class FeatureModel(
|
|||
|
||||
class ConfigValueModel(
|
||||
val coinMarketCapKey: String,
|
||||
val mercuryoWidgetId: String,
|
||||
val mercuryoSecret: String,
|
||||
val moonPayApiKey: String,
|
||||
val onramperApiKey: String,
|
||||
val moonPayApiSecretKey: String,
|
||||
val blockchairApiKey: String?,
|
||||
val blockchairAuthorizationToken: String?,
|
||||
val blockcypherTokens: Set<String>?,
|
||||
val infuraProjectId: String?,
|
||||
val appsFlyerDevKey: String,
|
||||
val shopifyShop: ShopifyShop?
|
||||
val shopifyShop: ShopifyShop?,
|
||||
val zendesk: ZendeskConfig?,
|
||||
)
|
||||
|
||||
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.tap.common.extensions.containsAny
|
|||
import com.tangem.tap.common.extensions.removeBy
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -188,31 +187,6 @@ class WarningMessagesManager(
|
|||
WarningMessage.Origin.Local
|
||||
)
|
||||
|
||||
fun restoreFundsWarning(): WarningMessage = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Temporary,
|
||||
priority = WarningMessage.Priority.Warning,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.alert_title,
|
||||
messageResId = R.string.alert_funds_restoration_message,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
buttonTextId = R.string.warning_button_learn_more
|
||||
)
|
||||
|
||||
const val REMAINING_SIGNATURES_WARNING = 10
|
||||
private const val RESTORE_FUNDS_GUIDE_URL_RU =
|
||||
"https://tangem.com/ru/kak-vosstanovit-tokeny-otpravlennye-ne-na-tot-adres-v-tangem-wallet"
|
||||
private const val RESTORE_FUNDS_GUIDE_URL_EN =
|
||||
"https://tangem.com/en/how-to-recover-crypto-sent-to-the-wrong-address-in-tangem-wallet"
|
||||
|
||||
fun getRestoreFundsGuideUrl(locale: String): String {
|
||||
return if (locale == Locale("ru").language) {
|
||||
RESTORE_FUNDS_GUIDE_URL_RU
|
||||
} else {
|
||||
RESTORE_FUNDS_GUIDE_URL_EN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package com.tangem.tap.domain.extensions
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Blockchain.Arbitrum
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeStatus
|
||||
import com.tangem.tap.store
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun CurrencyExchangeManager.buyIsAllowed(currency: Currency): Boolean {
|
||||
return this.status?.buyIsAllowed(currency) ?: false
|
||||
}
|
||||
|
||||
fun CurrencyExchangeManager.sellIsAllowed(currency: Currency): Boolean {
|
||||
return this.status?.sellIsAllowed(currency) ?: false
|
||||
}
|
||||
|
||||
fun CurrencyExchangeStatus.buyIsAllowed(currency: Currency): Boolean {
|
||||
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
|
||||
if (currency.blockchain == Arbitrum) return false
|
||||
if (!isBuyAllowed) return false
|
||||
|
||||
//TODO: temporary, for the 3.32 release, unlock all buy button
|
||||
return true
|
||||
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val blockchain = currency.blockchain
|
||||
when {
|
||||
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
|
||||
blockchain == Blockchain.Unknown -> false
|
||||
else -> availableToBuy.contains(currency.currencySymbol)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun CurrencyExchangeStatus.sellIsAllowed(currency: Currency): Boolean {
|
||||
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
|
||||
if (!isSellAllowed) return false
|
||||
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val blockchain = currency.blockchain
|
||||
when {
|
||||
blockchain.isTestnet() -> false
|
||||
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
|
||||
else -> availableToSell.contains(currency.currencySymbol)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,15 @@ import androidx.transition.TransitionInflater
|
|||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.domain.common.getTwinCardIdForUser
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.tap.features.feedback.FeedbackEmail
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -123,7 +126,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
|
|||
}
|
||||
|
||||
tvSendFeedback.setOnClickListener {
|
||||
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
|
||||
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
|
||||
}
|
||||
|
||||
tvWalletConnect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
|
||||
|
|
@ -131,6 +134,12 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
|
|||
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
|
||||
}
|
||||
|
||||
tvSupport.setOnClickListener {
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
|
||||
}
|
||||
|
||||
llManageSecurity.setOnClickListener {
|
||||
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
|
||||
tvSecurityAndPrivacy.setOnClickListener {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.SecurityAndPrivacy))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,380 +0,0 @@
|
|||
package com.tangem.tap.features.feedback
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdkLogger
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds
|
||||
import com.tangem.tap.common.extensions.sendEmail
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.wallet.R
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeedbackManager(
|
||||
val infoHolder: AdditionalEmailInfo,
|
||||
private val logCollector: TangemLogCollector,
|
||||
) {
|
||||
|
||||
private lateinit var activity: Activity
|
||||
|
||||
fun updateActivity(activity: Activity) {
|
||||
this.activity = activity
|
||||
}
|
||||
|
||||
fun send(emailData: EmailData, onFail: ((Exception) -> Unit)? = null) {
|
||||
if (!this::activity.isInitialized) return
|
||||
|
||||
emailData.prepare(infoHolder)
|
||||
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
|
||||
activity.sendEmail(
|
||||
email = getSupportEmail(),
|
||||
subject = activity.getString(emailData.subjectResId),
|
||||
message = emailData.joinTogether(activity, infoHolder),
|
||||
file = fileLog,
|
||||
onFail = onFail
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportEmail(): String {
|
||||
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
|
||||
S2C_SUPPORT_EMAIL
|
||||
} else {
|
||||
DEFAULT_SUPPORT_EMAIL
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLogFile(): File? {
|
||||
return try {
|
||||
val file = File(activity.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
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
|
||||
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
|
||||
}
|
||||
}
|
||||
|
||||
class TangemLogCollector : TangemSdkLogger {
|
||||
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
|
||||
private val logs = mutableListOf<String>()
|
||||
private val mutex = Object()
|
||||
|
||||
override fun log(message: () -> String, level: Log.Level) {
|
||||
val time = dateFormatter.format(Date())
|
||||
synchronized(mutex) {
|
||||
logs.add("$time: ${message()}\n")
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
|
||||
|
||||
fun clearLogs() {
|
||||
synchronized(mutex) { logs.clear() }
|
||||
}
|
||||
}
|
||||
|
||||
class AdditionalEmailInfo {
|
||||
class EmailWalletInfo(
|
||||
var blockchain: Blockchain = Blockchain.Unknown,
|
||||
var address: String = "",
|
||||
var explorerLink: String = "",
|
||||
var host: String = "",
|
||||
var derivationPath: String = "",
|
||||
)
|
||||
|
||||
var appVersion: String = ""
|
||||
|
||||
// card
|
||||
var cardId: String = ""
|
||||
var cardFirmwareVersion: String = ""
|
||||
var cardIssuer: String = ""
|
||||
var cardBlockchain: String = ""
|
||||
|
||||
// wallets
|
||||
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
|
||||
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
|
||||
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
|
||||
var signedHashesCount: String = ""
|
||||
|
||||
// device
|
||||
var phoneModel: String = Build.MODEL
|
||||
var osVersion: String = Build.VERSION.SDK_INT.toString()
|
||||
|
||||
// send error
|
||||
var destinationAddress: String = ""
|
||||
var amount: String = ""
|
||||
var fee: String = ""
|
||||
var token: String = ""
|
||||
|
||||
fun setAppVersion(context: Context) {
|
||||
try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
appVersion = pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCardInfo(data: ScanResponse) {
|
||||
cardId = data.card.cardId
|
||||
cardBlockchain = data.walletData?.blockchain ?: ""
|
||||
cardFirmwareVersion = data.card.firmwareVersion.stringValue
|
||||
cardIssuer = data.card.issuer.name
|
||||
signedHashesCount = data.card.wallets
|
||||
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
}
|
||||
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
walletsInfo.clear()
|
||||
tokens.clear()
|
||||
walletManagers.forEach { manager ->
|
||||
walletsInfo.add(
|
||||
EmailWalletInfo(
|
||||
blockchain = manager.wallet.blockchain,
|
||||
address = getAddress(manager.wallet),
|
||||
explorerLink = getExploreUri(manager.wallet),
|
||||
host = manager.currentHost,
|
||||
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
)
|
||||
if (manager.cardTokens.isNotEmpty()) {
|
||||
tokens[manager.wallet.blockchain] = manager.cardTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateOnSendError(wallet: Wallet, host: String, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) {
|
||||
onSendErrorWalletInfo = EmailWalletInfo(
|
||||
blockchain = wallet.blockchain,
|
||||
address = getAddress(wallet),
|
||||
explorerLink = getExploreUri(wallet),
|
||||
host = host,
|
||||
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
|
||||
this.destinationAddress = destinationAddress
|
||||
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
|
||||
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
|
||||
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
|
||||
}
|
||||
|
||||
private fun getAddress(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.address
|
||||
} else {
|
||||
val addresses = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${it.value}"
|
||||
}
|
||||
"Multiple address: $addresses"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExploreUri(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.getExploreUrl(wallet.address)
|
||||
} else {
|
||||
val links = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
|
||||
}
|
||||
"Multiple explorers links: $links"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EmailData {
|
||||
val subjectResId: Int
|
||||
val mainMessageResId: Int
|
||||
|
||||
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
|
||||
|
||||
fun prepare(infoHolder: AdditionalEmailInfo) {}
|
||||
|
||||
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
|
||||
|
||||
fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String {
|
||||
return StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(3)
|
||||
append(context.getString(getDataCollectionMessageResId()))
|
||||
appendLine()
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class RateCanBeBetterEmail : EmailData {
|
||||
override val subjectResId: Int = R.string.feedback_subject_rate_negative
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class ScanFailsEmail : EmailData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_scan_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
|
||||
|
||||
override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String = StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(4)
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class SendTransactionFailedEmail(
|
||||
val error: String
|
||||
) : EmailData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_tx_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendDelimiter()
|
||||
.appendTxFailedBlockchainInfo(error)
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class FeedbackEmail : EmailData {
|
||||
override val subjectResId: Int
|
||||
get() = if (isS2CCard) s2cSubject else tangemSubject
|
||||
override val mainMessageResId: Int
|
||||
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
|
||||
|
||||
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
|
||||
private val tangemMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private val s2cSubject: Int = R.string.feedback_subject_support
|
||||
private val s2cMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private var isS2CCard = false
|
||||
|
||||
override fun prepare(infoHolder: AdditionalEmailInfo) {
|
||||
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
|
||||
}
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendWalletsInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
class EmailDataBuilder(
|
||||
private val infoHolder: AdditionalEmailInfo
|
||||
) {
|
||||
val builder = StringBuilder()
|
||||
|
||||
fun appendDelimiter(): EmailDataBuilder {
|
||||
builder.appendDelimiter()
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendLine(count: Int = 1): EmailDataBuilder {
|
||||
builder.appendLine(count)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendCardInfo(): EmailDataBuilder {
|
||||
builder.appendKeyValue("Card ID", infoHolder.cardId)
|
||||
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
|
||||
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
|
||||
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendWalletsInfo(): EmailDataBuilder {
|
||||
infoHolder.walletsInfo.forEach {
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
|
||||
builder.appendKeyValue("Host", it.host)
|
||||
builder.appendKeyValue("Wallet address", it.address)
|
||||
builder.appendKeyValue("Derivation path", it.derivationPath)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
|
||||
infoHolder.tokens[it.blockchain]?.let { tokens ->
|
||||
builder.append("Tokens:")
|
||||
appendLine()
|
||||
tokens.forEach { token ->
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
builder.appendKeyValue("ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Contract address", token.contractAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendTxFailedBlockchainInfo(error: String): EmailDataBuilder {
|
||||
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
|
||||
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
|
||||
builder.appendKeyValue("Host", walletInfo.host)
|
||||
builder.appendKeyValue("Token", infoHolder.token)
|
||||
builder.appendKeyValue("Error", error)
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Source address", walletInfo.address)
|
||||
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
|
||||
builder.appendKeyValue("Amount", infoHolder.amount)
|
||||
builder.appendKeyValue("Fee", infoHolder.fee)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendPhoneInfo(): EmailDataBuilder {
|
||||
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
builder.appendKeyValue("OS version", infoHolder.osVersion)
|
||||
builder.appendKeyValue("App version", infoHolder.appVersion)
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String = builder.toString()
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
|
||||
return if (value.isNotBlank()) this.append("$key: $value\n") else this
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
|
||||
|
||||
private fun StringBuilder.appendLine(count: Int = 1): StringBuilder {
|
||||
return append(List(count) { "\n" }.joinToString(separator = ""))
|
||||
}
|
||||
|
|
@ -38,7 +38,9 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
|
|||
StoriesScreen(
|
||||
homeState,
|
||||
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
|
||||
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(getRegionProvider())) },
|
||||
onShopButtonClick = {
|
||||
store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode))
|
||||
},
|
||||
onSearchTokensClick = {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
|
||||
store.dispatch(TokensAction.AllowToAddTokens(false))
|
||||
|
|
|
|||
|
|
@ -44,5 +44,8 @@ class TelephonyManagerRegionProvider(context: Context) : RegionProvider {
|
|||
}
|
||||
|
||||
class LocaleRegionProvider : RegionProvider {
|
||||
override fun getRegion(): String? = Locale.current.region
|
||||
}
|
||||
override fun getRegion(): String = Locale.current.region
|
||||
}
|
||||
|
||||
const val RUSSIA_COUNTRY_CODE = "ru"
|
||||
const val BELARUS_COUNTRY_CODE = "by"
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.features.home.RegionProvider
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class HomeAction : Action {
|
||||
// from ui
|
||||
object ReadCard : HomeAction()
|
||||
data class GoToShop(val regionProvider: RegionProvider) : HomeAction()
|
||||
data class GoToShop(val userCountryCode: String?) : HomeAction()
|
||||
|
||||
// internal
|
||||
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import com.tangem.tap.common.redux.global.GlobalAction
|
|||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
|
|
@ -30,19 +32,19 @@ class HomeMiddleware {
|
|||
companion object {
|
||||
val handler = homeMiddleware
|
||||
|
||||
const val CARD_SHOP_URI = "http://cards.tangem.com/"
|
||||
const val BUY_WALLET_URL = "https://mv.tangem.com/"
|
||||
const val BUY_WALLET_URL = "https://tangem.com/ru/resellers/"
|
||||
}
|
||||
}
|
||||
|
||||
private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
private val homeMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is HomeAction.Init -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.InitCurrencyExchangeManager)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
if (action.shouldScanCard) {
|
||||
|
|
@ -55,8 +57,9 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
|
|||
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens))
|
||||
}
|
||||
is HomeAction.GoToShop -> {
|
||||
when (action.regionProvider.getRegion()?.toLowerCase()) {
|
||||
"ru" -> store.dispatchOpenUrl(BUY_WALLET_URL)
|
||||
when (action.userCountryCode) {
|
||||
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE ->
|
||||
store.dispatchOpenUrl(BUY_WALLET_URL)
|
||||
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
|
||||
}
|
||||
store.state.globalState.analyticsHandlers?.triggerEvent(
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class OnboardingManager(
|
|||
|
||||
data class OnboardingWalletBalance(
|
||||
val value: BigDecimal = BigDecimal.ZERO,
|
||||
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown, null),
|
||||
val currency: Currency = Currency.Blockchain(Blockchain.Unknown, null),
|
||||
val hasIncomingTransaction: Boolean = false,
|
||||
val state: ProgressState,
|
||||
val error: TapError? = null,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.note.redux
|
|||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.buyIsAllowed
|
||||
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.StateType
|
||||
|
|
@ -26,8 +25,8 @@ data class OnboardingNoteState(
|
|||
val progress: Int
|
||||
get() = steps.indexOf(currentStep)
|
||||
|
||||
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
|
||||
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
|
||||
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { _, _ ->
|
||||
store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.tap.features.onboarding.products.twins.redux
|
|||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.buyIsAllowed
|
||||
import com.tangem.tap.domain.twins.TwinCardsManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -56,8 +55,8 @@ data class TwinCardsState(
|
|||
val showAlert: Boolean
|
||||
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
|
||||
|
||||
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
|
||||
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
|
||||
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { _, _ ->
|
||||
store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,12 @@ private fun handleWalletAction(action: Action) {
|
|||
BlockchainNetwork(Blockchain.Bitcoin, result.data.card),
|
||||
BlockchainNetwork(Blockchain.Ethereum, result.data.card)
|
||||
)
|
||||
store.dispatchOnMain(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks))
|
||||
store.dispatchOnMain(
|
||||
WalletAction.MultiWallet.SaveCurrencies(
|
||||
blockchainNetworks = blockchainNetworks,
|
||||
cardId = result.data.card.cardId
|
||||
)
|
||||
)
|
||||
onboardingManager.activationStarted(updatedResponse.card.cardId)
|
||||
store.dispatch(OnboardingWalletAction.ProceedBackup)
|
||||
}
|
||||
|
|
@ -272,7 +277,11 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
BlockchainNetwork(Blockchain.Bitcoin, result.data),
|
||||
BlockchainNetwork(Blockchain.Ethereum, result.data)
|
||||
)
|
||||
store.dispatchOnMain(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks))
|
||||
store.dispatchOnMain(
|
||||
WalletAction.MultiWallet.SaveCurrencies(
|
||||
blockchainNetworks = blockchainNetworks, cardId = result.data.cardId
|
||||
)
|
||||
)
|
||||
if (backupService.currentState == BackupService.State.Finished) {
|
||||
store.dispatchOnMain(BackupAction.FinishBackup)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.tap.features.send.ui.dialogs
|
|||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
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.store
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -19,7 +19,7 @@ class SendTransactionFailsDialog {
|
|||
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)))
|
||||
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(dialog.errorMessage)))
|
||||
}
|
||||
setPositiveButton(R.string.common_no) { _, _ -> }
|
||||
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.tap.features.wallet.redux
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
|
|
@ -13,8 +17,8 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
|||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class WalletAction : Action {
|
||||
|
||||
|
|
@ -61,7 +65,9 @@ sealed class WalletAction : Action {
|
|||
MultiWallet()
|
||||
|
||||
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
|
||||
data class SaveCurrencies(val blockchainNetworks: List<BlockchainNetwork>) : MultiWallet()
|
||||
data class SaveCurrencies(
|
||||
val blockchainNetworks: List<BlockchainNetwork>, val cardId: String? = null
|
||||
) : MultiWallet()
|
||||
// object FindTokensInUse : MultiWallet()
|
||||
// object FindBlockchainsInUse : MultiWallet()
|
||||
|
||||
|
|
@ -104,8 +110,6 @@ sealed class WalletAction : Action {
|
|||
}
|
||||
|
||||
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
|
||||
|
||||
object RestoreFundsWarningClosed : Warnings()
|
||||
}
|
||||
|
||||
data class LoadFiatRate(
|
||||
|
|
@ -152,6 +156,7 @@ sealed class WalletAction : Action {
|
|||
object SignedHashesMultiWalletDialog : DialogAction()
|
||||
object ChooseTradeActionDialog : DialogAction()
|
||||
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
|
||||
object RussianCardholdersWarningDialog : DialogAction()
|
||||
|
||||
object Hide : DialogAction()
|
||||
}
|
||||
|
|
@ -162,8 +167,10 @@ sealed class WalletAction : Action {
|
|||
object EmptyWallet : WalletAction()
|
||||
|
||||
sealed class TradeCryptoAction : WalletAction() {
|
||||
object Buy : TradeCryptoAction()
|
||||
object Sell : TradeCryptoAction()
|
||||
data class Buy(
|
||||
val checkUserLocation: Boolean = true,
|
||||
) : TradeCryptoAction()
|
||||
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
|
||||
data class SendCrypto(
|
||||
val currencyId: String,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.tap.features.wallet.redux
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
|
|
@ -10,33 +9,22 @@ import com.tangem.blockchain.common.address.AddressType
|
|||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.domain.common.extensions.canHandleToken
|
||||
import com.tangem.tap.common.entities.Button
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.toQrCode
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
import com.tangem.tap.common.toggleWidget.WidgetState
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.extensions.buyIsAllowed
|
||||
import com.tangem.tap.domain.extensions.sellIsAllowed
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.PendingTransaction
|
||||
import com.tangem.tap.features.wallet.models.TotalBalance
|
||||
import com.tangem.tap.features.wallet.models.WalletRent
|
||||
import com.tangem.tap.features.wallet.models.WalletWarning
|
||||
import com.tangem.tap.features.wallet.models.hasPendingTransactions
|
||||
import com.tangem.tap.features.wallet.models.hasSendableAmounts
|
||||
import com.tangem.tap.features.wallet.models.isSendableAmount
|
||||
import com.tangem.tap.features.wallet.models.*
|
||||
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
|
||||
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
|
||||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class WalletState(
|
||||
val state: ProgressState = ProgressState.Done,
|
||||
|
|
@ -71,7 +59,7 @@ data class WalletState(
|
|||
|
||||
val shouldShowDetails: Boolean =
|
||||
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
|
||||
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
|
||||
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
|
||||
|
||||
val blockchains: List<Blockchain>
|
||||
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
|
||||
|
|
@ -320,16 +308,6 @@ data class WalletState(
|
|||
}
|
||||
}
|
||||
|
||||
sealed interface WalletDialog : StateDialog {
|
||||
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog
|
||||
object SignedHashesMultiWalletDialog : WalletDialog
|
||||
object ChooseTradeActionDialog : WalletDialog
|
||||
data class CurrencySelectionDialog(
|
||||
val currenciesList: List<FiatCurrency>,
|
||||
val currentAppCurrency: FiatCurrency,
|
||||
) : WalletDialog
|
||||
}
|
||||
|
||||
enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error }
|
||||
|
||||
enum class ErrorType { NoInternetConnection }
|
||||
|
|
@ -372,18 +350,21 @@ data class Artwork(
|
|||
}
|
||||
|
||||
data class TradeCryptoState(
|
||||
val sellingAllowed: Boolean = false,
|
||||
val buyingAllowed: Boolean = false,
|
||||
val isAvailableToSell: () -> Boolean = { false },
|
||||
val isAvailableToBuy: () -> Boolean = { false },
|
||||
) {
|
||||
companion object {
|
||||
fun from(
|
||||
exchangeManager: CurrencyExchangeManager?,
|
||||
walletData: WalletData
|
||||
): TradeCryptoState {
|
||||
val status = exchangeManager ?: return walletData.tradeCryptoState
|
||||
val exchanger = exchangeManager ?: return walletData.tradeCryptoState
|
||||
val currency = walletData.currency
|
||||
|
||||
return TradeCryptoState(status.sellIsAllowed(currency), status.buyIsAllowed(currency))
|
||||
return TradeCryptoState(
|
||||
isAvailableToSell = { exchanger.availableForSell(currency) },
|
||||
isAvailableToBuy = { exchanger.availableForBuy(currency) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,9 +77,8 @@ class MultiWalletMiddleware {
|
|||
)
|
||||
}
|
||||
is WalletAction.MultiWallet.SaveCurrencies -> {
|
||||
globalState.scanResponse?.card?.cardId?.let {
|
||||
currenciesRepository.saveCurrencies(it, action.blockchainNetworks)
|
||||
}
|
||||
val cardId = action.cardId ?: globalState.scanResponse?.card?.cardId ?: return
|
||||
currenciesRepository.saveCurrencies(cardId, action.blockchainNetworks)
|
||||
}
|
||||
is WalletAction.MultiWallet.TryToRemoveWallet -> {
|
||||
val currency = action.currency
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ 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.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20Tokens
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -24,51 +25,70 @@ class TradeCryptoMiddleware {
|
|||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
when (action) {
|
||||
is WalletAction.TradeCryptoAction.Buy -> startExchange(action)
|
||||
is WalletAction.TradeCryptoAction.Sell -> startExchange(action)
|
||||
is WalletAction.TradeCryptoAction.Buy -> proceedBuyAction(state, action)
|
||||
is WalletAction.TradeCryptoAction.Sell -> proceedSellAction()
|
||||
is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
|
||||
is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startExchange(action: WalletAction.TradeCryptoAction) {
|
||||
val selectedWalletData = store.state.walletState.getSelectedWalletData()
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager ?: return
|
||||
val addresses = selectedWalletData?.walletAddresses ?: return
|
||||
if (addresses.list.isEmpty()) return
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
|
||||
val defaultAddress = addresses.list[0].address
|
||||
val currency = selectedWalletData.currency
|
||||
val currencySymbol = selectedWalletData.currency.currencySymbol
|
||||
|
||||
val exchangeAction = if (action is WalletAction.TradeCryptoAction.Buy) {
|
||||
CurrencyExchangeManager.Action.Buy
|
||||
} else {
|
||||
CurrencyExchangeManager.Action.Sell
|
||||
private fun proceedBuyAction(
|
||||
state: () -> AppState?,
|
||||
action: WalletAction.TradeCryptoAction.Buy,
|
||||
) {
|
||||
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
|
||||
store.dispatchOnMain(
|
||||
WalletAction.DialogAction.RussianCardholdersWarningDialog
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (exchangeAction == CurrencyExchangeManager.Action.Buy &&
|
||||
currency is Currency.Token && currency.blockchain.isTestnet()
|
||||
) {
|
||||
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
|
||||
val exchangeManager = store.state.globalState.exchangeManager ?: return
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
|
||||
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
|
||||
if (addresses.isEmpty()) return
|
||||
|
||||
val currency = selectedWalletData.currency
|
||||
|
||||
if (currency is Currency.Token && currency.blockchain.isTestnet()) {
|
||||
val walletManager = store.state.walletState.getWalletManager(currency)
|
||||
if (walletManager !is EthereumWalletManager) {
|
||||
store.dispatchDebugErrorNotification("Testnet tokens available only for the ETH")
|
||||
store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum")
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) }
|
||||
scope.launch { exchangeManager.buyErc20TestnetTokens(walletManager, currency.token) }
|
||||
return
|
||||
}
|
||||
|
||||
exchangeManager.getUrl(
|
||||
action = exchangeAction,
|
||||
action = CurrencyExchangeManager.Action.Buy,
|
||||
blockchain = currency.blockchain,
|
||||
cryptoCurrencyName = currencySymbol,
|
||||
cryptoCurrencyName = currency.currencySymbol,
|
||||
fiatCurrencyName = appCurrency.code,
|
||||
walletAddress = defaultAddress
|
||||
walletAddress = addresses[0].address
|
||||
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
|
||||
}
|
||||
|
||||
private fun proceedSellAction() {
|
||||
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
|
||||
val exchangeManager = store.state.globalState.exchangeManager ?: return
|
||||
val appCurrency = store.state.globalState.appCurrency
|
||||
|
||||
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
|
||||
if (addresses.isEmpty()) return
|
||||
|
||||
val currency = selectedWalletData.currency
|
||||
|
||||
exchangeManager.getUrl(
|
||||
action = CurrencyExchangeManager.Action.Sell,
|
||||
blockchain = currency.blockchain,
|
||||
cryptoCurrencyName = currency.currencySymbol,
|
||||
fiatCurrencyName = appCurrency.code,
|
||||
walletAddress = addresses[0].address
|
||||
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
|
||||
}
|
||||
|
||||
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
|
||||
|
|
@ -89,7 +109,7 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
|
||||
private fun openReceiptUrl(transactionId: String) {
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager ?: return
|
||||
val exchangeManager = store.state.globalState.exchangeManager ?: return
|
||||
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
exchangeManager.getSellCryptoReceiptUrl(CurrencyExchangeManager.Action.Sell, transactionId)?.let {
|
||||
|
|
|
|||
|
|
@ -27,10 +27,13 @@ class WalletDialogsMiddleware {
|
|||
is WalletAction.DialogAction.ChooseCurrency -> {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.SelectAmountToSendDialog(
|
||||
amounts = action.amounts
|
||||
amounts = action.amounts
|
||||
)
|
||||
)
|
||||
}
|
||||
is WalletAction.DialogAction.RussianCardholdersWarningDialog -> {
|
||||
store.dispatchDialogShow(WalletDialog.RussianCardholdersWarningDialog)
|
||||
}
|
||||
is WalletAction.DialogAction.Hide -> {
|
||||
store.dispatchDialogHide()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.common.card.Card
|
|||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.tap.common.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.isGreaterThan
|
||||
|
|
@ -26,15 +25,15 @@ import com.tangem.tap.preferencesStorage
|
|||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WarningsMiddleware {
|
||||
fun handle(action: WalletAction.Warnings, globalState: GlobalState?) {
|
||||
when (action) {
|
||||
WalletAction.Warnings.Update -> setWarningMessages()
|
||||
is WalletAction.Warnings.Update -> setWarningMessages()
|
||||
is WalletAction.Warnings.CheckIfNeeded -> {
|
||||
showCardWarningsIfNeeded(globalState)
|
||||
val readyToShow = preferencesStorage.appRatingLaunchObserver.isReadyToShow()
|
||||
|
|
@ -66,9 +65,11 @@ class WarningsMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
is WalletAction.Warnings.RestoreFundsWarningClosed -> {
|
||||
preferencesStorage.saveRestoreFundsWarningClosed()
|
||||
}
|
||||
is WalletAction.Warnings.AppRating,
|
||||
is WalletAction.Warnings.CheckHashesCount,
|
||||
is WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount,
|
||||
is WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline,
|
||||
is WalletAction.Warnings.Set -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,9 +94,6 @@ class WarningsMiddleware {
|
|||
addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true)
|
||||
return@let
|
||||
}
|
||||
if (card.useOldStyleDerivation && !preferencesStorage.wasRestoreFundsWarningClosed()) {
|
||||
addWarningMessage(warning = WarningMessagesManager.restoreFundsWarning())
|
||||
}
|
||||
|
||||
showWarningLowRemainingSignaturesIfNeeded(card)
|
||||
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
|
||||
|
|
@ -190,6 +188,7 @@ class WarningsMiddleware {
|
|||
true
|
||||
)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,4 +30,6 @@ sealed interface WalletDialog : StateDialog {
|
|||
val messageRes: Int = R.string.token_details_unable_hide_alert_message
|
||||
val titleRes: Int = R.string.token_details_unable_hide_alert_title
|
||||
}
|
||||
|
||||
object RussianCardholdersWarningDialog : WalletDialog
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ class OnWalletLoadedReducer {
|
|||
val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState
|
||||
|
||||
val fiatCurrency = store.state.globalState.appCurrency
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager
|
||||
val exchangeManager = store.state.globalState.exchangeManager
|
||||
|
||||
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
|
||||
val formattedAmount = coinAmountValue?.toFormattedCurrencyString(
|
||||
|
|
@ -118,7 +118,7 @@ class OnWalletLoadedReducer {
|
|||
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
|
||||
|
||||
val fiatCurrencyName = store.state.globalState.appCurrency.code
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager
|
||||
val exchangeManager = store.state.globalState.exchangeManager
|
||||
|
||||
val token = wallet.getFirstToken()
|
||||
val tokenData = if (token != null) {
|
||||
|
|
|
|||
|
|
@ -6,17 +6,14 @@ import com.tangem.blockchain.common.Wallet
|
|||
import com.tangem.common.extensions.mapNotNullValues
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.toFiatRateString
|
||||
import com.tangem.tap.common.extensions.toFiatString
|
||||
import com.tangem.tap.common.extensions.toFiatValue
|
||||
import com.tangem.tap.common.extensions.toFormattedCurrencyString
|
||||
import com.tangem.tap.common.extensions.toFormattedFiatValue
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.getArtworkUrl
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.WalletRent
|
||||
import com.tangem.tap.features.wallet.redux.*
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
|
|
@ -32,8 +29,8 @@ import com.tangem.tap.features.wallet.redux.WalletStore
|
|||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.store
|
||||
import java.math.BigDecimal
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WalletReducer {
|
||||
companion object {
|
||||
|
|
@ -49,7 +46,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
|
||||
if (action !is WalletAction) return state.walletState
|
||||
|
||||
val exchangeManager = store.state.globalState.currencyExchangeManager
|
||||
val exchangeManager = store.state.globalState.exchangeManager
|
||||
var newState = state.walletState
|
||||
|
||||
when (action) {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
|
||||
private fun setupButtons() = with(binding) {
|
||||
rowButtons.onBuyClick = {
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy)
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy())
|
||||
}
|
||||
rowButtons.onSellClick = {
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Sell)
|
||||
|
|
@ -184,8 +184,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
}
|
||||
|
||||
rowButtons.updateButtonsVisibility(
|
||||
buyAllowed = selectedWallet.tradeCryptoState.buyingAllowed,
|
||||
sellAllowed = selectedWallet.tradeCryptoState.sellingAllowed,
|
||||
buyAllowed = selectedWallet.tradeCryptoState.isAvailableToBuy(),
|
||||
sellAllowed = selectedWallet.tradeCryptoState.isAvailableToSell(),
|
||||
sendAllowed = selectedWallet.mainButton.enabled,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
package com.tangem.tap.features.wallet.ui.adapters
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.tangem.tap.common.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.getActivity
|
||||
import com.tangem.tap.common.extensions.getColor
|
||||
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.feedback.RateCanBeBetterEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
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
|
||||
|
|
@ -89,26 +85,12 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
|
|||
binding.btnClose.hide()
|
||||
|
||||
val buttonAction =
|
||||
when {
|
||||
warning.titleResId == R.string.warning_important_security_info -> {
|
||||
when (warning.titleResId) {
|
||||
R.string.warning_important_security_info -> {
|
||||
View.OnClickListener {
|
||||
store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog)
|
||||
}
|
||||
}
|
||||
warning.messageResId == R.string.alert_funds_restoration_message -> {
|
||||
binding.btnClose.show()
|
||||
binding.btnClose.setOnClickListener {
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
store.dispatch(WalletAction.Warnings.RestoreFundsWarningClosed)
|
||||
}
|
||||
val locale = ConfigurationCompat
|
||||
.getLocales(Resources.getSystem().configuration)
|
||||
.get(0)
|
||||
val url = WarningMessagesManager.getRestoreFundsGuideUrl(locale.language)
|
||||
View.OnClickListener {
|
||||
store.dispatchOpenUrl(url)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
View.OnClickListener {
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
|
|
@ -135,7 +117,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
|
|||
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
|
||||
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
|
||||
store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail()))
|
||||
}
|
||||
binding.btnReallyCool.setOnClickListener {
|
||||
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ class ChooseTradeActionBottomSheetDialog(context: Context) : BottomSheetDialog(c
|
|||
}
|
||||
|
||||
binding!!.dialogBtnBuy.setOnClickListener {
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy)
|
||||
dismiss()
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy())
|
||||
}
|
||||
binding!!.dialogBtnSell.setOnClickListener {
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Sell)
|
||||
dismiss()
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Sell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.tap.features.wallet.ui.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
|
||||
|
||||
class RussianCardholdersWarningBottomSheetDialog(context: Context) : BottomSheetDialog(context) {
|
||||
|
||||
private var binding: DialogRussiansCardholdersWarningBinding? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DialogRussiansCardholdersWarningBinding
|
||||
.inflate(LayoutInflater.from(context))
|
||||
.also { setContentView(it.root) }
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
setOnDismissListener {
|
||||
binding = null
|
||||
store.dispatchDialogHide()
|
||||
}
|
||||
|
||||
binding?.btnYes?.setOnClickListener {
|
||||
store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false))
|
||||
dismiss()
|
||||
}
|
||||
binding?.btnNo?.setOnClickListener {
|
||||
store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INSTRUCTION_URL = "https://tangem.com/howtobuy.html"
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ package com.tangem.tap.features.wallet.ui.dialogs
|
|||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.feedback.ScanFailsEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.feedback.ScanFailsEmail
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ class ScanFailsDialog {
|
|||
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()))
|
||||
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
|
||||
}
|
||||
setNeutralButton(R.string.alert_troubleshooting_scan_card_ok) { _, _ -> }
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
package com.tangem.tap.features.wallet.ui.images
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
import coil.imageLoader
|
||||
import coil.load
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -29,57 +26,112 @@ fun loadCurrencyIcon(
|
|||
token: Token?,
|
||||
blockchain: Blockchain,
|
||||
) {
|
||||
when {
|
||||
token == null -> currencyImageView.loadIcon(
|
||||
iconUrl = getIconUrl(blockchain.toNetworkId()),
|
||||
placeholderRes = blockchain.getRoundIconRes(),
|
||||
CurrencyIconLoader(
|
||||
currencyImageView = currencyImageView,
|
||||
currencyTextView = currencyTextView,
|
||||
token = token,
|
||||
blockchain = blockchain
|
||||
)
|
||||
.load()
|
||||
}
|
||||
|
||||
private class CurrencyIconLoader(
|
||||
private val currencyImageView: ImageFilterView,
|
||||
private val currencyTextView: TextView,
|
||||
private val token: Token?,
|
||||
private val blockchain: Blockchain,
|
||||
) {
|
||||
fun load() {
|
||||
when {
|
||||
token == null && blockchain.isTestnet() -> loadTestnetBlockchainIcon()
|
||||
token == null -> loadBlockchainIcon()
|
||||
blockchain.isTestnet() -> loadTestnetTokenIcon()
|
||||
else -> loadTokenIcon()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBlockchainIcon() {
|
||||
loadBlockchainIconBase(
|
||||
onStart = {
|
||||
if (blockchain.isTestnet()) {
|
||||
currencyImageView.saturation = 0f
|
||||
} else {
|
||||
currencyImageView.colorFilter = null
|
||||
}
|
||||
currencyImageView.colorFilter = null
|
||||
}
|
||||
)
|
||||
token.symbol == QCX -> currencyImageView.load(R.drawable.ic_qcx)
|
||||
token.symbol == VOYR -> currencyImageView.load(R.drawable.ic_voyr)
|
||||
else -> currencyImageView.loadIcon(
|
||||
iconUrl = getTokenIconUrl(token, blockchain),
|
||||
}
|
||||
|
||||
private fun loadTestnetBlockchainIcon() {
|
||||
loadBlockchainIconBase(
|
||||
onStart = {
|
||||
currencyImageView.saturation = 0f
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadTokenIcon() {
|
||||
loadTokenIconBase(
|
||||
onStart = {
|
||||
currencyImageView.setColorFilter(it.getColor())
|
||||
},
|
||||
onSuccess = {
|
||||
currencyImageView.colorFilter = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadTestnetTokenIcon() {
|
||||
loadTokenIconBase(
|
||||
onStart = {
|
||||
currencyImageView.saturation = 0f
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun loadBlockchainIconBase(
|
||||
crossinline onStart: (Blockchain) -> Unit = {},
|
||||
crossinline onSuccess: (Blockchain) -> Unit = {},
|
||||
crossinline onError: (Blockchain) -> Unit = {},
|
||||
) {
|
||||
currencyImageView.loadIcon(
|
||||
data = getIconUrl(blockchain.toNetworkId()),
|
||||
placeholderRes = blockchain.getRoundIconRes(),
|
||||
onStart = { onStart(blockchain) },
|
||||
onSuccess = { onSuccess(blockchain) },
|
||||
onError = { onError(blockchain) },
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun loadTokenIconBase(
|
||||
crossinline onStart: (Token) -> Unit = {},
|
||||
crossinline onSuccess: (Token) -> Unit = {},
|
||||
crossinline onError: (Token) -> Unit = {},
|
||||
) {
|
||||
if (token == null) return
|
||||
|
||||
currencyImageView.loadIcon(
|
||||
data = getTokenIcon(token, blockchain),
|
||||
placeholderRes = R.drawable.shape_circle,
|
||||
onStart = {
|
||||
currencyTextView.text = token.symbol.take(1)
|
||||
currencyTextView.setTextColor(token.getTextColor())
|
||||
|
||||
if (blockchain.isTestnet()) {
|
||||
currencyImageView.saturation = 0f
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
currencyImageView.colorFilter = PorterDuffColorFilter(
|
||||
/* color = */
|
||||
token.getColor(),
|
||||
/* mode = */
|
||||
PorterDuff.Mode.SRC_ATOP,
|
||||
)
|
||||
onStart(token)
|
||||
},
|
||||
onSuccess = {
|
||||
if (!blockchain.isTestnet()) {
|
||||
currencyImageView.colorFilter = null
|
||||
}
|
||||
}
|
||||
currencyTextView.text = null
|
||||
onSuccess(token)
|
||||
},
|
||||
onError = { onError(token) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun ImageView.loadIcon(
|
||||
iconUrl: String?,
|
||||
data: Any?,
|
||||
placeholderRes: Int,
|
||||
crossinline onStart: () -> Unit = {},
|
||||
crossinline onSuccess: () -> Unit = {},
|
||||
crossinline onError: () -> Unit = {},
|
||||
) {
|
||||
ImageRequest.Builder(context)
|
||||
.data(iconUrl)
|
||||
.data(data)
|
||||
.placeholder(placeholderRes)
|
||||
.error(placeholderRes)
|
||||
.fallback(placeholderRes)
|
||||
|
|
@ -101,9 +153,15 @@ private inline fun ImageView.loadIcon(
|
|||
.also(context.imageLoader::enqueue)
|
||||
}
|
||||
|
||||
private fun getTokenIconUrl(token: Token, blockchain: Blockchain): String? {
|
||||
return token.id?.let(::getIconUrl)
|
||||
?: token.getCustomIconUrl()
|
||||
?: IconsUtil.getTokenIconUri(blockchain, token)
|
||||
?.toString()
|
||||
private fun getTokenIcon(token: Token, blockchain: Blockchain): Any? {
|
||||
return when (token.symbol) {
|
||||
QCX -> R.drawable.ic_qcx
|
||||
VOYR -> R.drawable.ic_voyr
|
||||
else -> {
|
||||
token.id?.let(::getIconUrl)
|
||||
?: token.getCustomIconUrl()
|
||||
?: IconsUtil.getTokenIconUri(blockchain, token)
|
||||
?.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,9 +118,8 @@ class SingleWalletView : WalletView {
|
|||
|
||||
setupButtonsType(state, binding)
|
||||
|
||||
val btnConfirm = if (state.tradeCryptoState.sellingAllowed ||
|
||||
state.tradeCryptoState.buyingAllowed
|
||||
) {
|
||||
val tradeState = state.tradeCryptoState
|
||||
val btnConfirm = if (tradeState.isAvailableToSell() || tradeState.isAvailableToBuy()) {
|
||||
lButtonsShort.btnConfirm
|
||||
} else {
|
||||
lButtonsLong.btnConfirmLong
|
||||
|
|
@ -148,10 +147,10 @@ class SingleWalletView : WalletView {
|
|||
}
|
||||
|
||||
private fun setupTradeButton(binding: FragmentWalletBinding, tradeCryptoState: TradeCryptoState) {
|
||||
val allowedToBuy = tradeCryptoState.buyingAllowed
|
||||
val allowedToSell = tradeCryptoState.sellingAllowed
|
||||
val allowedToBuy = tradeCryptoState.isAvailableToBuy()
|
||||
val allowedToSell = tradeCryptoState.isAvailableToSell()
|
||||
val action = when {
|
||||
allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy
|
||||
allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy()
|
||||
!allowedToBuy && allowedToSell -> WalletAction.TradeCryptoAction.Sell
|
||||
allowedToBuy && allowedToSell -> WalletAction.DialogAction.ChooseTradeActionDialog
|
||||
else -> null
|
||||
|
|
@ -176,9 +175,7 @@ class SingleWalletView : WalletView {
|
|||
}
|
||||
|
||||
private fun setupButtonsType(state: WalletData, binding: FragmentWalletBinding) = with(binding) {
|
||||
if (state.tradeCryptoState.sellingAllowed ||
|
||||
state.tradeCryptoState.buyingAllowed
|
||||
) {
|
||||
if (state.tradeCryptoState.isAvailableToSell() || state.tradeCryptoState.isAvailableToBuy()) {
|
||||
lButtonsLong.root.hide()
|
||||
lButtonsShort.root.show()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.tap.common.extensions.safeUpdate
|
|||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TangemSigner
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdk
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -18,57 +19,20 @@ import java.math.BigDecimal
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ExchangeService {
|
||||
suspend fun isBuyAllowed(): Boolean
|
||||
suspend fun availableToBuy(): List<String>
|
||||
suspend fun isSellAllowed(): Boolean
|
||||
suspend fun availableToSell(): List<String>
|
||||
}
|
||||
|
||||
interface ExchangeUrlBuilder {
|
||||
fun getUrl(
|
||||
action: CurrencyExchangeManager.Action,
|
||||
blockchain: Blockchain,
|
||||
cryptoCurrencyName: CryptoCurrencyName,
|
||||
fiatCurrencyName: String,
|
||||
walletAddress: String,
|
||||
): String?
|
||||
|
||||
fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String?
|
||||
|
||||
companion object {
|
||||
const val SCHEME = "https"
|
||||
const val URL_SELL = "sell.moonpay.com"
|
||||
const val SUCCESS_URL = "tangem://success.tangem.com"
|
||||
}
|
||||
}
|
||||
|
||||
class CurrencyExchangeManager(
|
||||
private val onramperService: ExchangeService,
|
||||
private val moonPayService: ExchangeService,
|
||||
private val buyService: ExchangeService,
|
||||
private val sellService: ExchangeService,
|
||||
) : ExchangeService, ExchangeUrlBuilder {
|
||||
|
||||
var status: CurrencyExchangeStatus? = null
|
||||
private set
|
||||
|
||||
suspend fun getStatus(): CurrencyExchangeStatus {
|
||||
val isBuyAllowed = isBuyAllowed()
|
||||
val isSellAllowed = isSellAllowed()
|
||||
val availableToBuy = availableToBuy()
|
||||
val availableToSell = availableToSell()
|
||||
status = CurrencyExchangeStatus(
|
||||
isBuyAllowed,
|
||||
isSellAllowed,
|
||||
availableToBuy,
|
||||
availableToSell,
|
||||
)
|
||||
return status!!
|
||||
override suspend fun update() {
|
||||
buyService.update()
|
||||
sellService.update()
|
||||
}
|
||||
|
||||
override suspend fun isBuyAllowed(): Boolean = onramperService.isBuyAllowed()
|
||||
override suspend fun availableToBuy(): List<String> = onramperService.availableToBuy()
|
||||
override suspend fun isSellAllowed(): Boolean = moonPayService.isSellAllowed()
|
||||
override suspend fun availableToSell(): List<String> = moonPayService.availableToSell()
|
||||
override fun isBuyAllowed(): Boolean = buyService.isBuyAllowed()
|
||||
override fun isSellAllowed(): Boolean = sellService.isSellAllowed()
|
||||
override fun availableForBuy(currency: Currency): Boolean = buyService.availableForBuy(currency)
|
||||
override fun availableForSell(currency: Currency): Boolean = sellService.availableForSell(currency)
|
||||
|
||||
override fun getUrl(
|
||||
action: Action,
|
||||
|
|
@ -96,22 +60,15 @@ class CurrencyExchangeManager(
|
|||
|
||||
private fun getExchangeUrlBuilder(action: Action): ExchangeUrlBuilder {
|
||||
return when (action) {
|
||||
Action.Buy -> onramperService
|
||||
Action.Sell -> moonPayService
|
||||
Action.Buy -> buyService
|
||||
Action.Sell -> sellService
|
||||
} as ExchangeUrlBuilder
|
||||
}
|
||||
|
||||
enum class Action { Buy, Sell }
|
||||
}
|
||||
|
||||
data class CurrencyExchangeStatus(
|
||||
val isBuyAllowed: Boolean,
|
||||
val isSellAllowed: Boolean,
|
||||
val availableToBuy: List<String>,
|
||||
val availableToSell: List<String>,
|
||||
)
|
||||
|
||||
suspend fun CurrencyExchangeManager.buyErc20Tokens(walletManager: EthereumWalletManager, token: Token) {
|
||||
suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: EthereumWalletManager, token: Token) {
|
||||
walletManager.safeUpdate()
|
||||
|
||||
val amountToSend = Amount(walletManager.wallet.blockchain)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.tap.network.exchangeServices
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
|
||||
interface ExchangeService {
|
||||
suspend fun update()
|
||||
fun isBuyAllowed(): Boolean
|
||||
fun isSellAllowed(): Boolean
|
||||
fun availableForBuy(currency: Currency):Boolean
|
||||
fun availableForSell(currency: Currency):Boolean
|
||||
}
|
||||
|
||||
interface ExchangeUrlBuilder {
|
||||
fun getUrl(
|
||||
action: CurrencyExchangeManager.Action,
|
||||
blockchain: Blockchain,
|
||||
cryptoCurrencyName: String,
|
||||
fiatCurrencyName: String,
|
||||
walletAddress: String,
|
||||
): String?
|
||||
|
||||
fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String?
|
||||
|
||||
companion object {
|
||||
const val SCHEME = "https"
|
||||
const val SUCCESS_URL = "tangem://success.tangem.com"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.tap.network.exchangeServices.mercuryo
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
|
||||
|
||||
private val CurrenciesUrl = "https://api.mercuryo.io/v1.6/lib/currencies"
|
||||
|
||||
|
||||
|
||||
interface MercuryoApi {
|
||||
|
||||
@GET("{apiVersion}/lib/currencies")
|
||||
suspend fun currencies(
|
||||
@Path("apiVersion") apiVersion: String,
|
||||
): MercuryoCurrenciesResponse
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://api.mercuryo.io/"
|
||||
const val API_VERSION = "v1.6"
|
||||
}
|
||||
}
|
||||
|
||||
data class MercuryoCurrenciesResponse(
|
||||
val status: Int,
|
||||
val data: Data,
|
||||
) {
|
||||
data class Data(
|
||||
val fiat: List<String>,
|
||||
val crypto: List<String>,
|
||||
val config: Config
|
||||
)
|
||||
|
||||
data class Config(
|
||||
val base: Map<String, String>,
|
||||
@Json(name = "has_withdrawal_fee")
|
||||
val hasWithdrawalFee: Map<String, Boolean>,
|
||||
@Json(name = "display_options")
|
||||
val displayOptions: Map<String, DisplayOption>,
|
||||
val icons: Map<String, Any>,
|
||||
)
|
||||
|
||||
data class DisplayOption(
|
||||
@Json(name = "fullname")
|
||||
val fullName: String,
|
||||
@Json(name = "total_digits")
|
||||
val totalDigits: Int,
|
||||
@Json(name = "display_digits")
|
||||
val displayDigits: Int,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.tap.network.exchangeServices.mercuryo
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.calculateSha512
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.common.services.performRequest
|
||||
import com.tangem.network.common.createRetrofitInstance
|
||||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class MercuryoService(
|
||||
private val apiVersion: String,
|
||||
private val mercuryoWidgetId: String,
|
||||
private val secret: String,
|
||||
) : ExchangeService, ExchangeUrlBuilder {
|
||||
|
||||
private val api: MercuryoApi = createRetrofitInstance(MercuryoApi.BASE_URL)
|
||||
.create(MercuryoApi::class.java)
|
||||
|
||||
private val blockchainsAvailableToBuy = mutableListOf<Blockchain>()
|
||||
private val tokensAvailableToBy = mutableMapOf<String, MutableList<Blockchain>>()
|
||||
|
||||
override suspend fun update() {
|
||||
when (val result = performRequest { api.currencies(apiVersion) }) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
if (response.status == 200) {
|
||||
// all currencies which can be bought
|
||||
val currenciesAvailableToBy = response.data.crypto
|
||||
// tokens which can be bought only from specific blockchain network
|
||||
val supportedTokensWithNetwork = response.data.config.base
|
||||
|
||||
currenciesAvailableToBy.forEach { currencyName ->
|
||||
val blockchain = blockchainFromCurrencyName(currencyName)
|
||||
if (blockchain == null) {
|
||||
// suppose its a token
|
||||
supportedTokensWithNetwork[currencyName]?.let {
|
||||
blockchainFromCurrencyName(it)
|
||||
}?.let { blockchainNetwork ->
|
||||
val supportedInBlockchainsNetwork = tokensAvailableToBy[currencyName]
|
||||
?: mutableListOf()
|
||||
supportedInBlockchainsNetwork.add(blockchainNetwork)
|
||||
tokensAvailableToBy[currencyName] = supportedInBlockchainsNetwork
|
||||
}
|
||||
} else {
|
||||
blockchainsAvailableToBuy.add(blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
blockchainsAvailableToBuy.clear()
|
||||
tokensAvailableToBy.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isBuyAllowed(): Boolean = true
|
||||
|
||||
override fun isSellAllowed(): Boolean = false
|
||||
|
||||
override fun availableForBuy(currency: Currency): Boolean {
|
||||
if (!isBuyAllowed()) return false
|
||||
|
||||
// blockchains which cant be defined by mercuryo service
|
||||
val unsupportedBlockchains = listOf(Blockchain.Unknown, Blockchain.Binance, Blockchain.Arbitrum)
|
||||
val blockchain = currency.blockchain
|
||||
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
when {
|
||||
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
|
||||
unsupportedBlockchains.contains(blockchain) -> false
|
||||
else -> {
|
||||
blockchainsAvailableToBuy.contains(currency.blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val supportedInBlockchains = tokensAvailableToBy[currency.currencySymbol] ?: return false
|
||||
supportedInBlockchains.contains(currency.blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun availableForSell(currency: Currency): Boolean = false
|
||||
|
||||
override fun getUrl(
|
||||
action: CurrencyExchangeManager.Action,
|
||||
blockchain: Blockchain,
|
||||
cryptoCurrencyName: CryptoCurrencyName,
|
||||
fiatCurrencyName: String,
|
||||
walletAddress: String
|
||||
): String {
|
||||
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
|
||||
|
||||
val builder = Uri.Builder()
|
||||
.scheme(ExchangeUrlBuilder.SCHEME)
|
||||
.authority("exchange.mercuryo.io")
|
||||
.appendQueryParameter("widget_id", mercuryoWidgetId)
|
||||
.appendQueryParameter("type", action.name.lowercase())
|
||||
.appendQueryParameter("currency", cryptoCurrencyName)
|
||||
.appendQueryParameter("address", walletAddress)
|
||||
.appendQueryParameter("signature", signature(walletAddress))
|
||||
.appendQueryParameter("fix_currency", "true")
|
||||
.appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL)
|
||||
|
||||
val url = builder.build().toString()
|
||||
return url
|
||||
}
|
||||
|
||||
private fun signature(address: String): String {
|
||||
return (address + secret).calculateSha512().toHexString().lowercase()
|
||||
}
|
||||
|
||||
|
||||
override fun getSellCryptoReceiptUrl(
|
||||
action: CurrencyExchangeManager.Action,
|
||||
transactionId: String
|
||||
): String? = null
|
||||
|
||||
private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) {
|
||||
"BNB" -> Blockchain.BSC
|
||||
"ETH" -> Blockchain.Ethereum
|
||||
else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() }
|
||||
}
|
||||
}
|
||||
|
|
@ -5,15 +5,15 @@ import android.util.Base64
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.common.services.performRequest
|
||||
import com.tangem.domain.common.extensions.withIOContext
|
||||
import com.tangem.network.common.createRetrofitInstance
|
||||
import com.tangem.tap.common.extensions.urlEncode
|
||||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.URL_SELL
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
|
@ -29,14 +29,14 @@ class MoonPayService(
|
|||
|
||||
private var status: MoonPayStatus? = null
|
||||
|
||||
private suspend fun updateStatus() {
|
||||
try {
|
||||
coroutineScope {
|
||||
override suspend fun update() {
|
||||
withIOContext {
|
||||
performRequest {
|
||||
val userStatusResult = performRequest { api.getUserStatus(apiKey) }
|
||||
if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult
|
||||
if (userStatusResult is Result.Failure) return@performRequest
|
||||
|
||||
val currenciesResult = performRequest { api.getCurrencies(apiKey) }
|
||||
if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult
|
||||
if (currenciesResult is Result.Failure) return@performRequest
|
||||
|
||||
val userStatus = (userStatusResult as Result.Success).data
|
||||
val currencies = (currenciesResult as Result.Success).data
|
||||
|
|
@ -65,29 +65,31 @@ class MoonPayService(
|
|||
|
||||
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
|
||||
}
|
||||
} catch (error: Error) {
|
||||
status = null
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isBuyAllowed(): Boolean = false
|
||||
override fun isBuyAllowed(): Boolean = false
|
||||
|
||||
override suspend fun availableToBuy(): List<String> = listOf()
|
||||
|
||||
override suspend fun isSellAllowed(): Boolean {
|
||||
refreshStatus()
|
||||
override fun isSellAllowed(): Boolean {
|
||||
return status?.responseUserStatus?.isSellAllowed ?: false
|
||||
}
|
||||
|
||||
override suspend fun availableToSell(): List<String> {
|
||||
refreshStatus()
|
||||
return status?.availableToSell ?: emptyList()
|
||||
}
|
||||
override fun availableForBuy(currency: Currency): Boolean = false
|
||||
|
||||
private suspend fun refreshStatus() {
|
||||
if (status == null) {
|
||||
updateStatus()
|
||||
override fun availableForSell(currency: Currency): Boolean {
|
||||
val availableForSell = status?.availableForSell ?: return false
|
||||
if (!isSellAllowed()) return false
|
||||
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val blockchain = currency.blockchain
|
||||
when {
|
||||
blockchain.isTestnet() -> false
|
||||
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
|
||||
else -> availableForSell.contains(currency.currencySymbol)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,10 +135,14 @@ class MoonPayService(
|
|||
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
|
||||
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val URL_SELL = "sell.moonpay.com"
|
||||
}
|
||||
}
|
||||
|
||||
private data class MoonPayStatus(
|
||||
val availableToSell: List<String>,
|
||||
val availableForSell: List<String>,
|
||||
val responseUserStatus: MoonPayUserStatus,
|
||||
val responseCurrencies: List<MoonPayCurrencies>
|
||||
)
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
package com.tangem.tap.network.exchangeServices.onramper
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface OnramperApi {
|
||||
@GET("gateways")
|
||||
suspend fun gateways(): GatewaysResponse
|
||||
|
||||
@GET("rate/{fromCurrency}/{toCurrency}/{paymentMethod}/{amount}")
|
||||
suspend fun rate(
|
||||
@Path("fromCurrency") fromCurrency: String,
|
||||
@Path("toCurrency") toCurrency: String,
|
||||
@Path("paymentMethod") paymentMethod: String,
|
||||
@Path("amount") amount: Int,
|
||||
): RateResponse
|
||||
|
||||
companion object {
|
||||
val BASE_URL = "https://onramper.tech/"
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GatewaysResponse(
|
||||
val gateways: List<OnramperGateway>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OnramperGateway(
|
||||
val identifier: String,
|
||||
val paymentMethods: List<String>,
|
||||
val fiatCurrencies: List<OnramperCurrency>,
|
||||
val cryptoCurrencies: List<OnramperCurrency>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OnramperCurrency(
|
||||
val id: String,
|
||||
val code: String,
|
||||
val precision: Int
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RateResponse(
|
||||
val identifier: String,
|
||||
val duration: OnramperDuration,
|
||||
val available: Boolean,
|
||||
val error: OnramperError? = null,
|
||||
val rate: Double? = null,
|
||||
val fees: Double? = null,
|
||||
val requiredKYC: List<String>? = null,
|
||||
val receivedCrypto: Double? = null,
|
||||
val nextStep: OnramperNextStep? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OnramperNextStep(
|
||||
val type: String,
|
||||
val url: String,
|
||||
val message: String,
|
||||
val extraData: List<OnramperExtraData>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OnramperExtraData(
|
||||
val type: String,
|
||||
val name: String,
|
||||
val humanName: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OnramperDuration(
|
||||
val seconds: Long,
|
||||
val message: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OnramperError(
|
||||
val type: String,
|
||||
val message: String,
|
||||
val limit: Double
|
||||
)
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
package com.tangem.tap.network.exchangeServices.onramper
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.common.services.performRequest
|
||||
import com.tangem.network.common.AddHeaderInterceptor
|
||||
import com.tangem.network.common.createRetrofitInstance
|
||||
import com.tangem.tap.common.extensions.urlEncode
|
||||
import com.tangem.tap.common.redux.global.CryptoCurrencyName
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SUCCESS_URL
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnramperService(
|
||||
val apiKey: String
|
||||
) : ExchangeService, ExchangeUrlBuilder {
|
||||
|
||||
private val api: OnramperApi by lazy {
|
||||
createRetrofitInstance(
|
||||
baseUrl = OnramperApi.BASE_URL,
|
||||
interceptors = listOf(
|
||||
AddHeaderInterceptor(mapOf("Authorization" to "Basic $apiKey")),
|
||||
)
|
||||
).create(OnramperApi::class.java)
|
||||
}
|
||||
|
||||
private var status: OnramperStatus? = null
|
||||
|
||||
private suspend fun updateStatus() {
|
||||
try {
|
||||
coroutineScope {
|
||||
val result = performRequest { api.gateways() }
|
||||
if (result is Result.Failure) return@coroutineScope result
|
||||
|
||||
val response = (result as Result.Success).data
|
||||
val currenciesToBuy = extractCurrenciesToBuy(response).sorted()
|
||||
val status = OnramperStatus(currenciesToBuy, response)
|
||||
this@OnramperService.status = status
|
||||
}
|
||||
} catch (error: Error) {
|
||||
status = null
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractCurrenciesToBuy(response: GatewaysResponse): List<String> {
|
||||
return response.gateways.map { gateway ->
|
||||
gateway.cryptoCurrencies.map { currency -> currency.code }
|
||||
}.flatten().toMutableSet().toList()
|
||||
}
|
||||
|
||||
override suspend fun isBuyAllowed(): Boolean {
|
||||
refreshStatus()
|
||||
return status != null
|
||||
}
|
||||
|
||||
override suspend fun availableToBuy(): List<String> {
|
||||
refreshStatus()
|
||||
return status?.availableToBuy ?: emptyList()
|
||||
}
|
||||
|
||||
private suspend fun refreshStatus() {
|
||||
if (status == null) {
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isSellAllowed(): Boolean = false
|
||||
|
||||
override suspend fun availableToSell(): List<String> = listOf()
|
||||
|
||||
override fun getUrl(
|
||||
action: CurrencyExchangeManager.Action,
|
||||
blockchain: Blockchain,
|
||||
cryptoCurrencyName: CryptoCurrencyName,
|
||||
fiatCurrency: String,
|
||||
walletAddress: String,
|
||||
): String? {
|
||||
var languageCode = Locale.getDefault().language
|
||||
if (languageCode.isEmpty()) languageCode = "en"
|
||||
|
||||
val builder = Uri.Builder()
|
||||
.scheme(SCHEME)
|
||||
.authority("widget.onramper.com")
|
||||
.appendQueryParameter("apiKey", this.apiKey.urlEncode())
|
||||
.appendQueryParameter("defaultCrypto", cryptoCurrencyName)
|
||||
.appendQueryParameter("wallets", "${blockchain.currency}:$walletAddress".urlEncode())
|
||||
.appendQueryParameter("redirectURL", SUCCESS_URL)
|
||||
.appendQueryParameter("defaultFiat", fiatCurrency)
|
||||
.appendQueryParameter("language", languageCode)
|
||||
|
||||
status?.apply {
|
||||
val gateways = responseGateways.gateways.joinToString(",") { it.identifier }.urlEncode()
|
||||
builder.appendQueryParameter("onlyGateways", gateways)
|
||||
|
||||
}
|
||||
|
||||
val url = builder.build().toString()
|
||||
return url
|
||||
}
|
||||
|
||||
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null
|
||||
}
|
||||
|
||||
private data class OnramperStatus(
|
||||
val availableToBuy: List<String>,
|
||||
val responseGateways: GatewaysResponse
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@ import android.content.Context
|
|||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
|
||||
|
||||
class PreferencesStorage(applicationContext: Application) {
|
||||
|
|
@ -53,20 +53,11 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
|
||||
}
|
||||
|
||||
fun wasRestoreFundsWarningClosed(): Boolean {
|
||||
return preferences.getBoolean(RESTORE_FUNDS_CLOSED_KEY, false)
|
||||
}
|
||||
|
||||
fun saveRestoreFundsWarningClosed() {
|
||||
preferences.edit { putBoolean(RESTORE_FUNDS_CLOSED_KEY, true) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES_NAME = "tapPrefs"
|
||||
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
|
||||
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
|
||||
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
|
||||
private const val RESTORE_FUNDS_CLOSED_KEY = "restoreFundsClosed"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_cross_rounded_24.xml
Normal file
9
app/src/main/res/drawable/ic_cross_rounded_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M16.3,16.3C16.7,15.9 16.7,15.3 16.3,14.9L12.4,11L16.3,7.1C16.7,6.7 16.7,6.1 16.3,5.7C15.9,5.3 15.3,5.3 14.9,5.7L11,9.6L7.1,5.7C6.7,5.3 6.1,5.3 5.7,5.7C5.3,6.1 5.3,6.7 5.7,7.1L9.6,11L5.7,14.9C5.3,15.3 5.3,15.9 5.7,16.3C6.1,16.7 6.7,16.7 7.1,16.3L11,12.4L14.9,16.3C15.3,16.7 15.9,16.7 16.3,16.3Z" />
|
||||
</vector>
|
||||
18
app/src/main/res/drawable/img_flag_ru_24.xml
Normal file
18
app/src/main/res/drawable/img_flag_ru_24.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="60"
|
||||
android:viewportHeight="60">
|
||||
<group>
|
||||
<clip-path android:pathData="M0,0h60v60h-60z" />
|
||||
<path
|
||||
android:fillColor="#F0F0F0"
|
||||
android:pathData="M30,60C46.569,60 60,46.569 60,30C60,13.432 46.569,0 30,0C13.432,0 0,13.432 0,30C0,46.569 13.432,60 30,60Z" />
|
||||
<path
|
||||
android:fillColor="#0052B4"
|
||||
android:pathData="M58.134,40.435C59.34,37.185 60,33.67 60,30C60,26.33 59.34,22.815 58.134,19.565H1.866C0.66,22.815 0,26.33 0,30C0,33.67 0.66,37.185 1.866,40.435L30,43.044L58.134,40.435Z" />
|
||||
<path
|
||||
android:fillColor="#D80027"
|
||||
android:pathData="M30,60C42.899,60 53.895,51.859 58.134,40.435H1.866C6.105,51.859 17.101,60 30,60Z" />
|
||||
</group>
|
||||
</vector>
|
||||
114
app/src/main/res/layout/dialog_russians_cardholders_warning.xml
Normal file
114
app/src/main/res/layout/dialog_russians_cardholders_warning.xml
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/backgroundWhite"
|
||||
android:minHeight="420dp"
|
||||
tools:layout_gravity="bottom">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_flag"
|
||||
android:layout_width="68dp"
|
||||
android:layout_height="68dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/img_flag_ru_24"
|
||||
app:layout_constraintBottom_toTopOf="@id/tv_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view_cross_outline"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/shape_circle"
|
||||
android:backgroundTint="@color/backgroundWhite"
|
||||
app:layout_constraintBottom_toBottomOf="@id/iv_cross"
|
||||
app:layout_constraintEnd_toEndOf="@id/iv_cross"
|
||||
app:layout_constraintStart_toStartOf="@id/iv_cross"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_cross" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_cross"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginEnd="-4dp"
|
||||
android:layout_marginBottom="-4dp"
|
||||
android:background="@drawable/shape_circle"
|
||||
android:backgroundTint="#FFEBEE"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_cross_rounded_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/iv_flag"
|
||||
app:layout_constraintEnd_toEndOf="@id/iv_flag"
|
||||
app:tint="#D80027" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="38dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="38dp"
|
||||
android:text="@string/russian_bank_card_warning_title"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/textBlack"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/tv_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/iv_flag" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="38dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="38dp"
|
||||
android:layout_marginBottom="38dp"
|
||||
android:text="@string/russian_bank_card_warning_subtitle"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/textBlack"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/btn_yes"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintVertical_bias="0" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_yes"
|
||||
style="@style/OnboardingButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="38dp"
|
||||
android:backgroundTint="@color/tapButtonColorBlack"
|
||||
android:text="@string/common_yes"
|
||||
android:textColor="@color/white"
|
||||
app:cornerRadius="14dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btn_no"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_no"
|
||||
style="@style/OnboardingButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:backgroundTint="@color/buttonGray"
|
||||
android:text="@string/common_no"
|
||||
android:textColor="@color/textBlack"
|
||||
app:cornerRadius="14dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/btn_yes"
|
||||
app:layout_constraintTop_toTopOf="@id/btn_yes" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -283,8 +283,22 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_support"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/details_ask_a_question"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
|
||||
app:drawableTint="@color/darkGray1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_wallet_connect" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@
|
|||
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" />
|
||||
|
|
@ -185,6 +187,8 @@
|
|||
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" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="common_no">Nein</string>
|
||||
<string name="common_save_changes">Änderungen speichern</string>
|
||||
<string name="common_warning">Warnung</string>
|
||||
<string name="common_retry">Erneut versuchen</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="common_no">Non</string>
|
||||
<string name="common_save_changes">Sauvegarder les modifications</string>
|
||||
<string name="common_warning">Alerte</string>
|
||||
<string name="common_retry">Réessayer</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="common_no">No</string>
|
||||
<string name="common_save_changes">Mantieni le modifiche</string>
|
||||
<string name="common_warning">Avviso</string>
|
||||
<string name="common_retry">Riprova</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Tangem</string>
|
||||
<string name="common_no">Нет</string>
|
||||
<string name="common_save_changes">Сохранить изменения</string>
|
||||
<string name="common_warning">Предупреждение</string>
|
||||
<string name="common_retry">Повторить попытку</string>
|
||||
|
|
@ -365,4 +364,5 @@
|
|||
<string name="feedback_preface_support">Привет, команда поддержки,</string>
|
||||
<string name="feedback_preface_tx_push_failed">Пожалуйста, расскажите нам больше о Вашей проблеме. Каждая деталь может быть полезной.</string>
|
||||
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
|
||||
<string name="common_no">Нет</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@
|
|||
<string name="main_no_backup_warning_title">Бэкап кошелька не был произведен</string>
|
||||
<string name="main_no_backup_warning_subtitle">Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру</string>
|
||||
|
||||
<string name="russian_bank_card_warning_title">Карты банков РФ в данный момент не принимаются</string>
|
||||
<string name="russian_bank_card_warning_subtitle">У вас есть карта банка другой страны или платежной системы UnionPay?</string>
|
||||
<string name="common_yes">Да</string>
|
||||
|
||||
<string name="details_ask_a_question">Задать вопрос</string>
|
||||
|
||||
<string name="security_and_privacy_title">Безопасность и приватность</string>
|
||||
<string name="security_and_privacy_security">Безопасность</string>
|
||||
<string name="security_and_privacy_privacy">Приватность</string>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
<string name="app_name" translatable="false">Tangem</string>
|
||||
|
||||
<string name="common_no">No</string>
|
||||
<string name="common_save_changes">Save changes</string>
|
||||
<string name="common_warning">Warning</string>
|
||||
<string name="common_retry">Retry</string>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@
|
|||
<string name="main_no_backup_warning_subtitle">To protect your assets, we advise you to carry out this procedure</string>
|
||||
<string name="wallet_hide_token" translatable="false">Remove token</string>
|
||||
|
||||
<string name="russian_bank_card_warning_title">Russian bank cards are not accepted at the moment</string>
|
||||
<string name="russian_bank_card_warning_subtitle">Do you have a bank card of another country or a UnionPay card?</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="common_no">No</string>
|
||||
|
||||
<string name="details_ask_a_question">Ask a question</string>
|
||||
<string name="security_and_privacy_title">Security & Privacy</string>
|
||||
<string name="security_and_privacy_security">Security</string>
|
||||
<string name="security_and_privacy_privacy">Privacy</string>
|
||||
|
|
|
|||
7
app/src/main/res/values/zendesk_styles.xml
Normal file
7
app/src/main/res/values/zendesk_styles.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="ZendeskTheme" parent="ZendeskSdkTheme.Light">
|
||||
<item name="colorPrimary">@color/darkGray6</item>
|
||||
<item name="colorPrimaryDark">@color/darkGray6</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -22,6 +22,7 @@ allprojects {
|
|||
mavenLocal()
|
||||
maven { url "https://nexus.tangem-tech.com/repository/maven-releases/" }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://zendesk.jfrog.io/zendesk/repo' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
ext.versions = [
|
||||
kotlin : '1.6.10',
|
||||
build_gradle: '7.1.3',
|
||||
kotlin : '1.6.10',
|
||||
build_gradle : '7.1.3',
|
||||
tamgem_card_sdk : 'develop-154',
|
||||
tamgem_blockchain_sdk: 'develop-93',
|
||||
]
|
||||
|
||||
ext.environmentConfig = [
|
||||
environment: "ENVIRONMENT",
|
||||
environment : "ENVIRONMENT",
|
||||
testActionEnabled: "TEST_ACTION_ENABLED",
|
||||
logEnabled : "LOG_ENABLED",
|
||||
]
|
||||
|
|
@ -59,10 +59,9 @@ dependencies {
|
|||
implementation implementation(project(path: ':network'))
|
||||
implementation implementation(project(path: ':common'))
|
||||
|
||||
implementation 'com.tangem:blockchain:develop-93'
|
||||
// implementation 'com.tangem:blockchain:0.0.1'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154'
|
||||
implementation "com.tangem:blockchain:${versions.tamgem_blockchain_sdk}"
|
||||
implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tamgem_card_sdk}"
|
||||
implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tamgem_card_sdk}"
|
||||
|
||||
// Kotlin coroutines
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ dependencies {
|
|||
implementation implementation(project(path: ':common'))
|
||||
|
||||
// Tangem sdk's
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
|
||||
implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tamgem_card_sdk}"
|
||||
|
||||
// Kotlin coroutines
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
|
|
|
|||
|
|
@ -43,4 +43,8 @@ data class CurrenciesResponse(val currencies: List<Currency>) {
|
|||
val unit: String, // $, €, ₽
|
||||
val type: String,
|
||||
) : TangemTechResponse
|
||||
}
|
||||
}
|
||||
|
||||
data class GeoResponse(
|
||||
val code: String
|
||||
) : TangemTechResponse
|
||||
|
|
@ -27,4 +27,6 @@ interface TangemTechApi {
|
|||
@GET("currencies")
|
||||
suspend fun currencies(): CurrenciesResponse
|
||||
|
||||
@GET("geo")
|
||||
suspend fun geo(): GeoResponse
|
||||
}
|
||||
|
|
@ -47,6 +47,10 @@ class TangemTechService {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun userCountry(): Result<GeoResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.geo() }
|
||||
}
|
||||
|
||||
suspend fun currencies(): Result<CurrenciesResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.currencies() }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue