Updated on 2026-08-14

This commit is contained in:
Tangem 2022-08-03 15:52:41 +03:00
commit cf6d14391f
37 changed files with 510 additions and 174 deletions

View file

@ -91,6 +91,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
windowInsetsController.isAppearanceLightStatusBars = true
windowInsetsController.isAppearanceLightNavigationBars = true
supportFragmentManager.registerFragmentLifecycleCallbacks(
NavBarInsetsFragmentLifecycleCallback(),
true
)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}

View file

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

View file

@ -8,6 +8,7 @@ import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.domain.DomainLayer
import com.tangem.network.common.MoshiConverter
@ -108,7 +109,21 @@ class TapApplication : Application(), ImageLoaderFactory {
val infoHolder = AdditionalFeedbackInfo()
infoHolder.setAppVersion(this)
val logWriter = TangemLogCollector()
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
)
val logWriter = TangemLogCollector(
levels = logLevels,
messageFormatter = LogFormat.StairsFormatter()
)
Log.addLogger(logWriter)
store.dispatch(GlobalAction.SetFeedbackManager(FeedbackManager(infoHolder, logWriter)))
@ -126,4 +141,5 @@ class TapApplication : Application(), ImageLoaderFactory {
data class LogConfig(
val coil: Boolean = BuildConfig.DEBUG,
val storeAction: Boolean = BuildConfig.DEBUG,
val zendesk: Boolean = BuildConfig.DEBUG,
)

View file

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

View file

@ -6,18 +6,19 @@ import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.logConfig
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import com.zendesk.logger.Logger
import timber.log.Timber
import zendesk.chat.Chat
import zendesk.chat.ChatConfiguration
import zendesk.chat.ChatEngine
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
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]
@ -30,14 +31,14 @@ class FeedbackManager(
context: Context,
zendeskConfig: ZendeskConfig,
) {
Zendesk.INSTANCE.init(
/* context = */ context,
/* zendeskUrl = */ zendeskConfig.url,
/* applicationId = */ zendeskConfig.appId,
/* oauthClientId = */ zendeskConfig.clientId,
Chat.INSTANCE.init(
context,
zendeskConfig.accountKey,
zendeskConfig.appId
)
Support.INSTANCE.init(Zendesk.INSTANCE)
Zendesk.INSTANCE.setIdentity(AnonymousIdentity())
// Zendesk logs
Logger.setLoggable(logConfig.zendesk)
}
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
@ -57,11 +58,8 @@ class FeedbackManager(
fun openChat(feedbackData: FeedbackData) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
RequestListActivity.builder()
.show(
/* context = */ activity,
/* configurations = */ buildConfigs(activity, feedbackData)
)
setChatVisitorNote(activity, feedbackData)
showMessagingActivity(activity)
}
}
@ -84,20 +82,30 @@ class FeedbackManager(
}
}
private fun buildConfigs(
private fun setChatVisitorNote(
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(),
)
) {
Chat.INSTANCE.providers()
?.profileProvider()
?.setVisitorNote(feedbackData.joinTogether(context, infoHolder))
}
private fun showMessagingActivity(context: Context) {
MessagingActivity.builder()
.withMultilineResponseOptionsEnabled(false)
.withBotLabelStringRes(R.string.chat_bot_name)
.withBotAvatarDrawable(R.mipmap.ic_launcher)
.withEngines(ChatEngine.engine())
.show(context, buildChatConfig())
}
private fun buildChatConfig(): Configuration {
return ChatConfiguration.builder()
.withOfflineFormEnabled(true)
.withAgentAvailabilityEnabled(true)
.withPreChatFormEnabled(false)
.build()
}
private fun getSupportEmail(): String {

View file

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

View file

@ -148,14 +148,14 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
val techService = store.state.domainNetworks.tangemTechService
when (val result = techService.userCountry()) {
is Result.Success -> {
store.dispatch(
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = result.data.code.lowercase()
)
)
}
is Result.Failure -> {
store.dispatch(
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase()
)

View file

@ -11,6 +11,8 @@ data class ZendeskConfig(
val appId: String,
@Json(name = "zendeskClientId")
val clientId: String,
@Json(name = "zendeskAccountKey")
val accountKey: String,
@Json(name = "zendeskUrl")
val url: String,
)

View file

@ -91,7 +91,8 @@ class ConfigManager(
blockchairApiKey = values.blockchairApiKey,
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
infuraProjectId = values.infuraProjectId,
tronGridApiKey = values.tronGridApiKey
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,

View file

@ -27,6 +27,7 @@ class ConfigValueModel(
val appsFlyerDevKey: String,
val shopifyShop: ShopifyShop?,
val zendesk: ZendeskConfig?,
val tronGridApiKey: String,
)
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {

View file

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

View file

@ -104,56 +104,25 @@ class DemoConfig {
?: Amount(BigDecimal.ZERO, blockchain).copy()
private fun getReleaseIds(): List<String> {
return (releaseDemoCardIds +
releaseDemoCardIds_19042022 +
testDemoCardIds).distinct()
return (releaseDemoCardIds + testDemoCardIds).distinct()
}
private val releaseDemoCardIds = mutableListOf(
// Tangem Wallet:
"AC01000000041100",
"AC01000000042462",
"AC01000000041647",
"AC01000000041621",
"AC01000000041217",
// === Not from the Google Sheet table ===
"FB10000000000196", // Note BTC
"FB20000000000186", // Note ETH
"FB30000000000176", // Wallet
"AC01000000041225",
"AC01000000041209",
"AC01000000041092",
"AC01000000041472",
"AC01000000041662",
"AC01000000045754",
"AC01000000045960",
// Tangem Note BTC:
"AB01000000046530",
"AB01000000046720",
"AB01000000046746",
"AB01000000046498",
"AB01000000046753",
"AB01000000049608",
"AB01000000046761",
"AB01000000049574",
"AB01000000046605",
"AB01000000046571",
"AB01000000046704",
"AB01000000046647",
// Tangem Note Ethereum:
"AB02000000051000",
"AB02000000050986",
"AB02000000051026",
"AB02000000051042",
"AB02000000051091",
"AB02000000051083",
"AB02000000050960",
"AB02000000051034",
"AB02000000050911",
"AB02000000051133",
"AB02000000051158",
"AB02000000051059",
)
// https://tangem.slack.com/archives/GMXC6PP71/p1650360610759269
private val releaseDemoCardIds_19042022 = listOf(
// Wallet
// === Mvideo ===
// Wallet
"AC01000000045754",
"AC01000000041662",
"AC01000000041647",
@ -186,7 +155,58 @@ class DemoConfig {
"AC01000000013497",
"AC01000000013836",
"AC01000000013505",
// Note BTC
"AC03000000046693",
"AC03000000046685",
"AC03000000046677",
"AC03000000046669",
"AC03000000046651",
"AC03000000046644",
"AC03000000046636",
"AC03000000046628",
"AC03000000046610",
"AC03000000046602",
"AC03000000046594",
"AC03000000046586",
"AC03000000046578",
"AC03000000046560",
"AC03000000046552",
"AC03000000046545",
"AC03000000046537",
"AC03000000046529",
"AC03000000046511",
"AC03000000046800",
"AC03000000046792",
"AC03000000046784",
"AC03000000046776",
"AC03000000046768",
"AC03000000046750",
"AC03000000046743",
"AC03000000046735",
"AC03000000046727",
"AC03000000046446",
"AC03000000046438",
"AC03000000046412",
"AC03000000046388",
"AC03000000046370",
"AC03000000046354",
"AC03000000046347",
"AC03000000046339",
"AC03000000046321",
"AC03000000046172",
"AC03000000046396",
"AC03000000046404",
"AC03000000046701",
"AC03000000046420",
"AC03000000046719",
"AC03000000046503",
"AC03000000046495",
"AC03000000046487",
"AC03000000046362",
"AC03000000046479",
"AC03000000046461",
"AC03000000046453",
// Note BTC
"AB01000000059608",
"AB01000000046647",
"AB01000000046571",
@ -219,7 +239,58 @@ class DemoConfig {
"AB01000000015782",
"AB01000000022598",
"AB01000000022580",
// Note ETH
"AB01000000005688",
"AB07000000005696",
"AB07000000005902",
"AB07000000005910",
"AB07000000005928",
"AB07000000005936",
"AB07000000005944",
"AB07000000005993",
"AB07000000005985",
"AB07000000005977",
"AB07000000005969",
"AB07000000005951",
"AB07000000005605",
"AB07000000005803",
"AB07000000005811",
"AB07000000005829",
"AB07000000005837",
"AB07000000005845",
"AB07000000005852",
"AB07000000005860",
"AB07000000005878",
"AB07000000005886",
"AB07000000005894",
"AB07000000005704",
"AB07000000005712",
"AB07000000005720",
"AB07000000005738",
"AB07000000005746",
"AB07000000005514",
"AB07000000005522",
"AB07000000005563",
"AB07000000005571",
"AB07000000005589",
"AB07000000005597",
"AB07000000005613",
"AB07000000005621",
"AB07000000005639",
"AB07000000005647",
"AB07000000005654",
"AB07000000005662",
"AB07000000005670",
"AB07000000005530",
"AB07000000005548",
"AB07000000005555",
"AB07000000005753",
"AB07000000005761",
"AB07000000005779",
"AB07000000005787",
"AB07000000005795",
"AB07000000005506",
// Note ETH
"AB02000000051083",
"AB02000000051059",
"AB02000000051158",
@ -252,6 +323,123 @@ class DemoConfig {
"AB02000000020252",
"AB02000000018652",
"AB02000000018561",
"AB08000000009481",
"AB08000000009473",
"AB08000000009705",
"AB08000000009897",
"AB08000000009689",
"AB08000000009671",
"AB08000000009465",
"AB08000000009457",
"AB08000000009440",
"AB08000000009432",
"AB08000000009424",
"AB08000000009416",
"AB08000000009408",
"AB08000000009390",
"AB08000000009374",
"AB08000000009382",
"AB08000000009267",
"AB08000000009275",
"AB08000000009283",
"AB08000000009291",
"AB08000000009309",
"AB08000000009317",
"AB08000000009325",
"AB08000000009333",
"AB08000000009341",
"AB08000000009358",
"AB08000000009366",
"AB08000000009077",
"AB08000000009143",
"AB08000000009168",
"AB08000000009184",
"AB08000000009192",
"AB08000000009200",
"AB08000000009226",
"AB08000000009218",
"AB08000000009234",
"AB08000000009242",
"AB08000000008574",
"AB08000000009069",
"AB08000000008525",
"AB08000000009051",
"AB08000000009135",
"AB08000000009150",
"AB08000000009176",
"AB08000000009085",
"AB08000000009093",
"AB08000000009101",
"AB08000000009119",
"AB08000000009127",
"AB08000000009259",
// === Technopark ===
// Wallet
"AC01000000044120",
"AC01000000044997",
"AC01000000044989",
"AC01000000043494",
"AC01000000043486",
"AC01000000044187",
"AC01000000043148",
"AC01000000044013",
"AC01000000043973",
"AC01000000044815",
"AC01000000044807",
"AC01000000043809",
"AC01000000043833",
"AC01000000043460",
"AC01000000043064",
"AC01000000044138",
"AC01000000044500",
"AC01000000044492",
"AC01000000044260",
"AC01000000044278",
// Note BTC
"AB01000000049864",
"AB01000000053239",
"AB01000000053056",
"AB01000000054237",
"AB01000000054245",
"AB01000000054211",
"AB01000000054229",
"AB01000000053189",
"AB01000000054195",
"AB01000000050797",
"AB01000000053833",
"AB01000000052124",
"AB01000000051605",
"AB01000000052223",
"AB01000000052207",
"AB01000000052199",
"AB01000000047785",
"AB01000000047850",
"AB01000000047868",
"AB01000000048288",
// Note ETH
"AB02000000049715",
"AB02000000049848",
"AB02000000049814",
"AB02000000049863",
"AB02000000049871",
"AB02000000049855",
"AB02000000049285",
"AB02000000049277",
"AB02000000049558",
"AB02000000049889",
"AB02000000049988",
"AB02000000049707",
"AB02000000049699",
"AB02000000049897",
"AB02000000049905",
"AB02000000049913",
"AB02000000049251",
"AB02000000049533",
"AB02000000049541",
"AB02000000049830",
)
private val testDemoCardIds = listOf(
@ -281,7 +469,6 @@ class DemoTransactionSender(
}
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val dataToSign = randomString(32).toByteArray()
val signerResponse = signer.sign(

View file

@ -10,8 +10,8 @@ data class PendingTransaction(
val type: PendingTransactionType,
) {
val address: String? = when (type) {
PendingTransactionType.Incoming -> transactionData.destinationAddress
PendingTransactionType.Outgoing -> transactionData.sourceAddress
PendingTransactionType.Incoming -> transactionData.sourceAddress
PendingTransactionType.Outgoing -> transactionData.destinationAddress
PendingTransactionType.Unknown -> null
}

View file

@ -4,7 +4,6 @@ sealed class WalletWarning(
val showingPosition: Int,
) {
object TransactionInProgress : WalletWarning(10)
object SolanaTokensUnsupported : WalletWarning(20)
data class BalanceNotEnoughForFee(val blockchainFullName: String) : WalletWarning(30)
data class Rent(val walletRent: WalletRent) : WalletWarning(40)
}

View file

@ -8,6 +8,8 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
@ -395,19 +397,10 @@ data class WalletData(
}
fun assembleWarnings(): List<WalletWarning> {
val blockchain = currency.blockchain
val walletWarnings = mutableListOf<WalletWarning>()
if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) {
walletWarnings.add(WalletWarning.TransactionInProgress)
}
if (currency.isBlockchain()) {
if (blockchain == Blockchain.Solana || blockchain == Blockchain.SolanaTestnet) {
val card = store.state.globalState.scanResponse?.card
if (card?.canHandleToken(blockchain) == false) {
walletWarnings.add(WalletWarning.SolanaTokensUnsupported)
}
}
}
if (walletRent != null) {
walletWarnings.add(WalletWarning.Rent(walletRent))
}

View file

@ -21,9 +21,6 @@ class WalletWarningConverter(
message.blockchainFullName, message.blockchainFullName
)
}
WalletWarning.SolanaTokensUnsupported -> {
context.getString(R.string.warning_token_send_unsupported_message)
}
WalletWarning.TransactionInProgress -> {
context.getString(R.string.wallet_pending_transaction_warning)
}

View file

@ -9,7 +9,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getRoundIconRes
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
@ -118,7 +118,7 @@ class WalletAdapter
badgeCustomBalance.isVisible = isCustomCurrency
ivBlockchain.isVisible = wallet.currency.isToken()
ivBlockchain.setImageResource(wallet.currency.blockchain.getGreyedOutIconRes())
ivBlockchain.setImageResource(wallet.currency.blockchain.getRoundIconRes())
cardWallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))

View file

@ -24,7 +24,6 @@ class CurrencyIconView @JvmOverloads constructor(
init {
elevation = 0f
cardElevation = 0f
radius = dpToPx(8f)
background = null
radius = dpToPx(6f)
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.tap.features.wallet.ui.images
import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Shader
import androidx.annotation.Px
import androidx.core.graphics.applyCanvas
import coil.size.Size
import coil.size.pxOrElse
import coil.transform.Transformation
class RoundedCornersCropTransformation(
@Px val radiusPx: Float,
) : Transformation {
override val cacheKey: String = "${javaClass.name}-$radiusPx"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
val outputWidth = size.width.pxOrElse { Int.MAX_VALUE }
val outputHeight = size.height.pxOrElse { Int.MAX_VALUE }
val outputSize = minOf(outputWidth, outputHeight)
val output = Bitmap.createBitmap(outputSize, outputSize, Bitmap.Config.ARGB_8888)
val rect = RectF(
/* left = */ 0f,
/* top = */ 0f,
/* right = */ outputWidth.toFloat(),
/* bottom = */ outputHeight.toFloat()
)
val matrix = Matrix().apply {
setTranslate(
/* dx = */ (outputWidth - input.width) * .5f,
/* dy = */ (outputHeight - input.height) * .5f
)
}
val bitmapShader = BitmapShader(
/* bitmap = */ input,
/* tileX = */ Shader.TileMode.DECAL,
/* tileY = */ Shader.TileMode.DECAL
).apply {
setLocalMatrix(matrix)
}
val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
shader = bitmapShader
}
return output.applyCanvas {
drawRoundRect(
/* rect = */ rect,
/* rx = */ radiusPx,
/* ry = */ radiusPx,
/* paint = */ paint
)
}
}
}