Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-24 13:45:56 +03:00
commit ea661f55ab
250 changed files with 6169 additions and 949 deletions

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
@ -13,7 +13,6 @@ sealed class MainScreen(
class ScreenOpened : MainScreen("Screen opened")
class ButtonScanCard : MainScreen("Button - Scan Card")
class ButtonMyWallets : MainScreen("Button - My Wallets")
class ButtonBuy : MainScreen("Button - Buy")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.extensions.filterNotNull

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class MyWallets(
event: String,

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
@ -72,26 +72,8 @@ sealed class Onboarding(
class SetupFinished : Twins("Twin Setup Finished")
}
class PinScreenOpened : Onboarding("Onboarding", "PIN screen opened")
class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code")
class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened")
class ButtonConnect : Onboarding("Onboarding", "Button - Connect")
class PinCodeSet : Onboarding("Onboarding", "PIN Code Set")
class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened")
class KYCStarted : Onboarding("Onboarding", "KYC Started")
class KYCInProgress : Onboarding("Onboarding", "KYC In Progress")
class KYCRejected : Onboarding("Onboarding", "KYC Rejected")
class ClaimScreenOpened : Onboarding("Onboarding", "Claim Screen Opened")
class ButtonClaim : Onboarding("Onboarding", "Button - Claim")
class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim Was Successfully")
class ButtonChat : Onboarding("Onboarding", "Button - Chat")
class NotEnoughGasError : Onboarding("Onboarding", "Not Enough Gas Error")
class CardNotPassedError : Onboarding("Onboarding", "Card Not Passed Error")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
category = "Onboarding / Biometric",
event = "Enable Biometric",

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.ui.details.SocialNetwork
/**

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.extensions.filterNotNull
/**

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam.CurrencyType
/**

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]

View file

@ -1,8 +1,8 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
import com.appsflyer.AFInAppEventType
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.events.Shop

View file

@ -1,9 +1,9 @@
package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.ErrorEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
import com.tangem.tap.common.analytics.events.Shop

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse

View file

@ -1,14 +1,14 @@
package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.models.scan.ScanResponse
/**
[REDACTED_AUTHOR]
*/
class LinkedCardContextInterceptor(
private val scanResponse: ScanResponse,
scanResponse: ScanResponse,
val parent: LinkedCardContextInterceptor? = null,
) : ParamsInterceptor {

View file

@ -9,6 +9,7 @@ import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.converters.TopUpEventConverter
@ -17,7 +18,6 @@ import com.tangem.tap.common.extensions.copy
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.models.Currency

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Store
import java.io.File
class ChatManager(
private val preferencesStorage: PreferencesDataSource,
@ -20,7 +21,7 @@ class ChatManager(
) {
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
fun open(config: ChatConfig, feedbackDataBuilder: (Context) -> String) {
fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) {
val opener = openers.getOrPut(config) {
when (config) {
is SprinklrConfig -> SprinklrChatOpener(getSprinklrUserId(), config, store, foregroundActivityObserver)
@ -28,7 +29,10 @@ class ChatManager(
}
}
opener.open(feedbackDataBuilder)
opener.open(
createFeedbackFile = createFeedbackFile,
createLogsFile = createLogsFile,
)
}
private fun getZendeskUserId(): String {

View file

@ -1,7 +1,8 @@
package com.tangem.tap.common.chat.opener
import android.content.Context
import java.io.File
internal interface ChatOpener {
fun open(feedbackDataBuilder: (Context) -> String)
fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?)
}

View file

@ -2,14 +2,15 @@ package com.tangem.tap.common.chat.opener.implementation
import android.content.Context
import android.content.Intent
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.sprinklr.redux.SprinklrAction
import com.tangem.tap.features.sprinklr.ui.SprinklrActivity
import com.tangem.tap.withForegroundActivity
import org.rekotlin.Store
import java.io.File
internal class SprinklrChatOpener(
private val userId: String,
@ -17,7 +18,7 @@ internal class SprinklrChatOpener(
private val store: Store<AppState>,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
override fun open(feedbackDataBuilder: (Context) -> String) {
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
store.dispatch(SprinklrAction.Init(userId, config))
foregroundActivityObserver.withForegroundActivity { activity ->
val intent = Intent(activity, SprinklrActivity::class.java)

View file

@ -2,34 +2,34 @@ package com.tangem.tap.common.chat.opener.implementation
import android.content.Context
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.models.ZendeskConfig
import com.tangem.domain.common.LogConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.datasource.config.models.ZendeskConfig
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import com.zendesk.logger.Logger
import zendesk.chat.Chat
import zendesk.chat.ChatConfiguration
import zendesk.chat.ChatEngine
import zendesk.chat.ChatProvidersConfiguration
import zendesk.chat.VisitorInfo
import timber.log.Timber
import zendesk.chat.*
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
import java.io.File
internal class ZendeskChatOpener(
private val userId: String,
private val config: ZendeskConfig,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
private var isInitialized = false
override fun open(feedbackDataBuilder: (Context) -> String) {
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
foregroundActivityObserver.withForegroundActivity { activity ->
initZendeskIfNeeded(activity.applicationContext)
setChatVisitorInfo()
setChatVisitorNote(feedbackDataBuilder(activity))
showMessagingActivity(activity)
sendFeedbackFile(createFeedbackFile(activity))
sendLogsFile(createLogsFile(activity))
}
}
@ -48,8 +48,20 @@ internal class ZendeskChatOpener(
ChatProvidersConfiguration.builder().withVisitorInfo(visitorInfo).build()
}
private fun setChatVisitorNote(note: String) {
Chat.INSTANCE.providers()?.profileProvider()?.setVisitorNote(note)
private fun sendFeedbackFile(feedbackFile: File?) {
if (isInitialized && feedbackFile != null) {
Chat.INSTANCE.providers()?.chatProvider()?.sendFile(feedbackFile) { _, bytesUploaded, _ ->
Timber.d("Log file sent", "bytesUploaded: $bytesUploaded")
}
}
}
private fun sendLogsFile(logsFile: File?) {
if (isInitialized && logsFile != null) {
Chat.INSTANCE.providers()?.chatProvider()?.sendFile(logsFile) { _, bytesUploaded, _ ->
Timber.d("Log file sent", "bytesUploaded: $bytesUploaded")
}
}
}
private fun showMessagingActivity(context: Context) {

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.di.domain.wallets
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,4 +18,10 @@ object WalletsDomainModule {
fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase {
return GetWalletsUseCase(walletsStateHolder = walletsStateHolder)
}
@Provides
@ViewModelScoped
fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase {
return SaveWalletUseCase(walletsStateHolder = walletsStateHolder)
}
}

View file

@ -24,13 +24,17 @@ fun BigDecimal.toFormattedString(
df.decimalFormatSymbols = symbols
df.maximumFractionDigits = decimals
df.minimumFractionDigits = 0
df.isGroupingUsed = false
df.isGroupingUsed = true
df.roundingMode = roundingMode
return df.format(this)
}
/**
* To formatted crypto currency string
* Specific method because there is no crypto currency codes in Locale
*/
@Suppress("MagicNumber")
fun BigDecimal.toFormattedCurrencyString(
fun BigDecimal.toFormattedCryptoCurrencyString(
decimals: Int,
currency: String,
roundingMode: RoundingMode = RoundingMode.DOWN,
@ -41,16 +45,34 @@ fun BigDecimal.toFormattedCurrencyString(
} else {
decimals
}
try {
val locale = Locale.getDefault()
val formatter = NumberFormat.getCurrencyInstance(locale)
// first create currency instance for "USD" with Locale default to replace currency to crypto later
Currency.getInstance("USD")?.let { currencyTmp ->
formatter.currency = currencyTmp
formatter.maximumFractionDigits = decimalsForRounding
formatter.minimumFractionDigits = 0
formatter.isGroupingUsed = true
formatter.roundingMode = roundingMode
// cause formatter created for USD, replace Currency with Crypto symbol on right by Locale place
return formatter.format(this).replace(currencyTmp.getSymbol(locale), "$currency")
}
} catch (e: IllegalArgumentException) {
Timber.e(e, "can't parse currency")
}
// if something went wrong - use old way to format
val formattedAmount = this.toFormattedString(
decimals = decimalsForRounding,
roundingMode = roundingMode,
locale = Locale.getDefault(),
)
return "$formattedAmount $currency"
return "$formattedAmount $currency"
}
fun BigDecimal.toFiatRateString(fiatCurrencyName: String, fiatCode: String): String {
try {
val formatter = NumberFormat.getCurrencyInstance()
val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault())
Currency.getInstance(fiatCode)?.let { currency ->
formatter.currency = currency
formatter.maximumFractionDigits = 2
@ -91,7 +113,7 @@ fun BigDecimal.toFormattedFiatValue(
formatWithSpaces: Boolean = false,
): String {
try {
val formatter = NumberFormat.getCurrencyInstance()
val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault())
Currency.getInstance(fiatCode)?.let { currency ->
formatter.currency = currency
formatter.maximumFractionDigits = 2

View file

@ -1,17 +1,12 @@
package com.tangem.tap.common.feedback
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.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
class AdditionalFeedbackInfo {
class EmailWalletInfo(

View file

@ -1,9 +1,9 @@
package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.chat.ChatManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.foregroundActivityObserver
@ -22,32 +22,61 @@ class FeedbackManager(
private val chatManager: ChatManager,
) {
private var sessionFeedbackFile: File? = null
private var sessionLogsFile: File? = null
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
val fileLog = createLogFile(activity)
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = fileLog,
file = getLogFile(activity),
onFail = onFail,
)
}
}
fun openChat(config: ChatConfig, feedbackData: FeedbackData) {
chatManager.open(config) { context ->
feedbackData.run {
chatManager.open(
config = config,
createLogsFile = ::getLogFile,
createFeedbackFile = { context -> getFeedbackFile(context, feedbackData) },
)
}
private fun getFeedbackFile(context: Context, feedbackData: FeedbackData): File? {
return try {
if (sessionFeedbackFile != null) {
return sessionFeedbackFile
}
val file = File(context.filesDir, FEEDBACK_FILE)
file.delete()
file.createNewFile()
val feedback = feedbackData.run {
prepare(infoHolder)
joinTogether(context, infoHolder)
}
val fileWriter = FileWriter(file)
fileWriter.write(feedback)
fileWriter.close()
sessionFeedbackFile = file
sessionFeedbackFile
} catch (ex: Exception) {
Timber.e(ex, "Can't create the logs file")
null
}
}
private fun createLogFile(context: Context): File? {
private fun getLogFile(context: Context): File? {
return try {
val file = File(context.filesDir, "logs.txt")
if (sessionLogsFile != null) {
return sessionLogsFile
}
val file = File(context.filesDir, LOGS_FILE)
file.delete()
file.createNewFile()
@ -57,7 +86,8 @@ class FeedbackManager(
fileWriter.write(stringWriter.toString())
fileWriter.close()
logCollector.clearLogs()
file
sessionLogsFile = file
sessionLogsFile
} catch (ex: Exception) {
Timber.e(ex, "Can't create the logs file")
null
@ -75,5 +105,7 @@ class FeedbackManager(
companion object {
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
const val FEEDBACK_FILE = "feedback.txt"
const val LOGS_FILE = "logs.txt"
}
}

View file

@ -4,8 +4,8 @@ import android.content.Context
import android.util.Log
import coil.ImageLoader
import coil.util.Logger
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
private const val COIL_LOG_TAG = "COIL"
@ -18,14 +18,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
logger(CoilTimberLogger())
okHttpClient {
OkHttpClient.Builder()
.addNetworkInterceptor(
HttpLoggingInterceptor { message ->
Timber.tag(COIL_LOG_TAG).d(message)
}
.apply {
level = HttpLoggingInterceptor.Level.BODY
},
)
.addNetworkInterceptor(createNetworkLoggingInterceptor())
.build()
}
}

View file

@ -8,6 +8,7 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE
import me.dm7.barcodescanner.zxing.ZXingScannerView
@ -16,11 +17,15 @@ import me.dm7.barcodescanner.zxing.ZXingScannerView
[REDACTED_AUTHOR]
*/
class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
private lateinit var mScannerView: ZXingScannerView
override fun onCreate(state: Bundle?) {
super.onCreate(state)
mScannerView = ZXingScannerView(this)
mScannerView = ZXingScannerView(this).apply {
setFormats(listOf(BarcodeFormat.QR_CODE))
}
setContentView(mScannerView)
if (!permissionIsGranted()) requestPermission()

View file

@ -3,7 +3,8 @@ package com.tangem.tap.common.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import org.rekotlin.Middleware
class AccessCodeRequestPolicyMiddleware {
@ -19,9 +20,8 @@ class AccessCodeRequestPolicyMiddleware {
}
private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
scanResponse.card.isAccessCodeSet,
store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet,
)
}
}