Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-16 13:58:49 +03:00
commit 6311ce2e3c
4 changed files with 118 additions and 37 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.analytics.handlers.appsflyer
import android.content.Context
import com.appsflyer.AppsFlyerConversionListener
import com.appsflyer.AppsFlyerLib
import com.appsflyer.attribution.AppsFlyerRequestListener
import com.tangem.core.analytics.api.EventLogger
@ -17,18 +18,49 @@ internal class AppsFlyerClient(
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
private val eventConverter = UnderscoreAnalyticsEventConverter()
private val logEventListener = object : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
}
override fun onError(p0: Int, p1: String) {
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
}
}
init {
appsFlyerLib.init(key, null, context)
appsFlyerLib.start(context, key, object : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.d("AppsFlyer initialized successfully")
val conversionListener = object : AppsFlyerConversionListener {
override fun onConversionDataSuccess(p0: Map<String?, Any?>?) {
Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
}
override fun onConversionDataFail(p0: String?) {
Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
}
override fun onAppOpenAttribution(p0: Map<String?, String?>?) {
Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
}
override fun onAttributionFailure(p0: String?) {
Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
}
}
override fun onError(p0: Int, p1: String) {
Timber.e("AppsFlyer initialization error: $p0, $p1")
}
})
appsFlyerLib.run {
setAppId(context.packageName)
setDebugLog(true)
init(key, conversionListener, context)
Timber.i("Starting AppsFlyer SDK")
start(context, key, object : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.i("AppsFlyer initialized successfully")
}
override fun onError(p0: Int, p1: String) {
Timber.e("AppsFlyer initialization error: $p0, $p1")
}
})
Timber.i("AppsFlyer SDK started")
}
}
override fun setUserId(userId: String) {
@ -40,10 +72,12 @@ internal class AppsFlyerClient(
}
override fun logEvent(event: String, params: Map<String, String>) {
Timber.tag("AppsFlyer").i("Logging event: $event with params: $params")
appsFlyerLib.logEvent(
context,
event,
eventConverter.convertEventParams(params),
logEventListener,
)
}
}

View file

@ -6,19 +6,26 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.AppsFlyerOnlyEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import timber.log.Timber
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
) : AnalyticsHandler, AnalyticsUserIdHandler {
init {
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler created")
}
override fun id(): String = ID
override fun send(event: AnalyticsEvent) {
when (event) {
is AppsFlyerOnlyEvent -> {
Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
client.logEvent(event.id, event.params)
}
is AppsFlyerIncludedEvent -> {
Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
client.logEvent(
event = AnalyticsEvent(category = event.category, event = event.appsFlyerReplacedEvent).id,
params = event.params,
@ -40,10 +47,16 @@ class AppsFlyerAnalyticsHandler(
}
class Builder : AnalyticsHandlerBuilder {
init {
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
}
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler = AppsFlyerAnalyticsHandler(
client = if (data.logConfig.isAppsflyerLogEnabled) {
Timber.tag("AppsFlyer").i("AppsFlyer log enabled, mock client created")
AppsFlyerLogClient(data.jsonConverter)
} else {
Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created")
AppsFlyerClient(data.application, data.config.appsFlyerApiKey)
},
)

View file

@ -1119,6 +1119,7 @@ internal class SwapModel @Inject constructor(
}
}
@Suppress("LongMethod")
private fun onTokenSelect(id: String) {
val tokens = dataState.tokensDataState ?: return
val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id)
@ -1149,6 +1150,8 @@ internal class SwapModel @Inject constructor(
coin = newToken,
isFromCurrency = true,
)
} else {
fromTokenBalanceJobHolder.cancel()
}
} else {
fromToken = initialFromStatus
@ -1167,6 +1170,8 @@ internal class SwapModel @Inject constructor(
coin = newToken,
isFromCurrency = false,
)
} else {
toTokenBalanceJobHolder.cancel()
}
}
@ -1221,6 +1226,7 @@ internal class SwapModel @Inject constructor(
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun subscribeToCoinBalanceUpdates(
userWalletId: UserWalletId,
coin: CryptoCurrency.Coin,
@ -1243,20 +1249,25 @@ internal class SwapModel @Inject constructor(
).getOrNull() ?: currencyStatus,
)
uiState = if (isFromCurrency) {
dataState = dataState.copy(
fromCryptoCurrency = currencyStatus,
fromAccount = account,
)
stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus)
} else {
dataState = dataState.copy(
toCryptoCurrency = currencyStatus,
toAccount = account,
)
stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus)
uiState = when {
isFromCurrency && currencyStatus.currency.id == dataState.fromCryptoCurrency?.currency?.id -> {
dataState = dataState.copy(
fromCryptoCurrency = currencyStatus,
fromAccount = account,
)
stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus)
}
!isFromCurrency && currencyStatus.currency.id == dataState.toCryptoCurrency?.currency?.id -> {
dataState = dataState.copy(
toCryptoCurrency = currencyStatus,
toAccount = account,
)
stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus)
}
else -> {
uiState
}
}
startLoadingQuotesFromLastState(isSilent = true)
}
} else {
@ -1276,14 +1287,19 @@ internal class SwapModel @Inject constructor(
).getOrNull() ?: status,
)
uiState = if (isFromCurrency) {
dataState = dataState.copy(fromCryptoCurrency = status)
stateBuilder.updateSendCurrencyBalance(uiState, status)
} else {
dataState = dataState.copy(toCryptoCurrency = status)
stateBuilder.updateReceiveCurrencyBalance(uiState, status)
uiState = when {
isFromCurrency && status.currency.id == dataState.fromCryptoCurrency?.currency?.id -> {
dataState = dataState.copy(fromCryptoCurrency = status)
stateBuilder.updateSendCurrencyBalance(uiState, status)
}
!isFromCurrency && status.currency.id == dataState.toCryptoCurrency?.currency?.id -> {
dataState = dataState.copy(toCryptoCurrency = status)
stateBuilder.updateReceiveCurrencyBalance(uiState, status)
}
else -> {
uiState
}
}
startLoadingQuotesFromLastState(isSilent = true)
}
}.flowOn(dispatchers.main)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Surface
@ -9,13 +10,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock
@Composable
internal fun TangemPayCardMainBlock(
@ -33,8 +38,9 @@ internal fun TangemPayCardMainBlock(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Image(
painter = painterResource(R.drawable.img_visa_36),
@ -42,20 +48,15 @@ internal fun TangemPayCardMainBlock(
modifier = Modifier.size(36.dp),
)
Spacer(Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = stringResourceSafe(R.string.tangempay_payment_account),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
Spacer(Modifier.height(6.dp))
Text(
text = state.lastFourDigits.resolveReference(),
style = TangemTheme.typography.caption2,
@ -64,6 +65,7 @@ internal fun TangemPayCardMainBlock(
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.End,
) {
Text(
@ -72,14 +74,30 @@ internal fun TangemPayCardMainBlock(
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
Text(
text = state.balanceSymbol.resolveReference(),
style = TangemTheme.typography.body2,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.End,
)
}
}
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCardMainBlockPreview() {
TangemThemePreview {
TangemPayMainScreenBlock(
TangemPayState.Card(
lastFourDigits = TextReference.Str("*1234"),
balanceText = TextReference.Str("$ 0.00"),
balanceSymbol = TextReference.Str("USDC"),
onClick = {},
),
isBalanceHidden = false,
)
}
}