From 87eff1d6cef15537609b7afe9b95e6db666d0894 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 23 Jun 2026 23:24:14 +0800 Subject: [PATCH 01/12] feat(wallet): add home ad banners --- .../main/java/one/mixin/android/Constants.kt | 2 - .../android/api/response/WalletHomeBanner.kt | 88 ++++++++++ .../android/api/service/ReferralService.kt | 7 + .../java/one/mixin/android/di/AppModule.kt | 2 +- .../android/repository/ReferralRepository.kt | 28 ++++ .../android/ui/home/web3/Web3ViewModel.kt | 4 + .../android/ui/setting/LogAndDebugFragment.kt | 70 +++++--- .../mixin/android/ui/wallet/WalletFragment.kt | 18 +- .../ui/wallet/WalletHomeBannerActionTarget.kt | 34 ++++ .../ui/wallet/WalletHomeClassicFragment.kt | 129 +++++++++++++- .../ui/wallet/WalletHomePreferences.kt | 8 + .../ui/wallet/WalletHomePrivacyFragment.kt | 69 +++++++- .../android/ui/wallet/WalletViewModel.kt | 4 + .../android/ui/wallet/home/WalletHomeState.kt | 6 + .../ui/wallet/home/components/BannerCards.kt | 117 +++++++++++-- .../util/analytics/AnalyticsTracker.kt | 13 ++ app/src/main/res/drawable/ic_deafult.xml | 94 +++++++++++ .../main/res/layout/fragment_log_debug.xml | 12 ++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../api/response/WalletHomeBannerTest.kt | 54 ++++++ .../ui/conversation/PerpsTradeActionTest.kt | 45 +++++ .../ui/wallet/WalletHomePreferencesTest.kt | 19 +++ .../ui/wallet/home/WalletHomeBuilderTest.kt | 157 ++++++++++++++++++ 24 files changed, 937 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/one/mixin/android/api/response/WalletHomeBanner.kt create mode 100644 app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt create mode 100644 app/src/main/res/drawable/ic_deafult.xml create mode 100644 app/src/test/java/one/mixin/android/api/response/WalletHomeBannerTest.kt create mode 100644 app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt diff --git a/app/src/main/java/one/mixin/android/Constants.kt b/app/src/main/java/one/mixin/android/Constants.kt index 9796e1ec9e..3d4887dd03 100644 --- a/app/src/main/java/one/mixin/android/Constants.kt +++ b/app/src/main/java/one/mixin/android/Constants.kt @@ -536,13 +536,11 @@ object Constants { const val ROUTE_BOT_USER_ID = "61cb8dd4-16b1-4744-ba0c-7b2d2e52fc59" const val REFERRAL_BOT_USER_ID = "b35af74d-cca6-400c-a62b-5a7e659de91e" - const val SAFE_BOT_USER_ID = "b5418449-9ed6-4979-a690-82690949c542" const val ROUTE_BOT_URL = "https://api.route.mixin.one" const val REFERRAL_API_URL = "https://api.reward.mixin.one" - const val GOOGLE_PAY = "googlepay" const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_PRODUCTION diff --git a/app/src/main/java/one/mixin/android/api/response/WalletHomeBanner.kt b/app/src/main/java/one/mixin/android/api/response/WalletHomeBanner.kt new file mode 100644 index 0000000000..07d104c375 --- /dev/null +++ b/app/src/main/java/one/mixin/android/api/response/WalletHomeBanner.kt @@ -0,0 +1,88 @@ +package one.mixin.android.api.response + +import com.google.gson.annotations.SerializedName + +data class WalletHomeBanner( + @SerializedName(value = "banner_id", alternate = ["id"]) + val bannerId: String? = null, + @SerializedName("placement") + val placement: String? = null, + @SerializedName("lang") + val lang: String? = null, + @SerializedName("icon_url") + val iconUrl: String? = null, + @SerializedName("title") + val title: String? = null, + @SerializedName("description") + val description: String? = null, + @SerializedName("actions") + val actions: List? = emptyList(), + @SerializedName("action_url") + val actionUrl: String? = null, + @SerializedName("tracking_key") + val trackingKey: String? = null, + @SerializedName("status") + val status: String? = null, + @SerializedName("start_at") + val startAt: String? = null, + @SerializedName("end_at") + val endAt: String? = null, + @SerializedName("chains") + val chains: List? = emptyList(), + @SerializedName("priority") + val priority: Int = 0, + @SerializedName("created_at") + val createdAt: String? = null, + @SerializedName("updated_at") + val updatedAt: String? = null, +) { + val key: String + get() = bannerId.takeUnless(String?::isNullOrBlank) + ?: actionUrl.takeUnless(String?::isNullOrBlank) + ?: title.takeUnless(String?::isNullOrBlank) + ?: iconUrl.orEmpty() + + val hasVisualContent: Boolean + get() = !title.isNullOrBlank() || + !description.isNullOrBlank() || + visibleActions.isNotEmpty() || + !iconUrl.isNullOrBlank() + + val visibleActions: List + get() = actions.orEmpty() + .firstOrNull { !it.label.isNullOrBlank() && !it.action.isNullOrBlank() } + ?.let(::listOf) + .orEmpty() + + val hasButtonStyle: Boolean + get() = visibleActions.isNotEmpty() + + val isActive: Boolean + get() = status.isNullOrBlank() || status.equals(BANNER_STATUS_ACTIVE, ignoreCase = true) + + companion object { + const val BANNER_STATUS_ACTIVE = "active" + const val BANNER_STATUS_INACTIVE = "inactive" + } +} + +data class WalletHomeBannerAction( + @SerializedName("label") + val label: String? = null, + @SerializedName("action") + val action: String? = null, +) + +fun Set.syncedWalletHomeClosedBannerIds(remoteBanners: List): Set { + val remoteKeys = remoteBanners.mapNotNull { it.key.takeIf(String::isNotBlank) }.toSet() + return filter { remoteKeys.contains(it) }.toSet() +} + +fun List.visibleWalletHomeBanners(closedBannerIds: Set): List = + filter { banner -> + banner.key.isNotBlank() && + banner.isActive && + banner.hasVisualContent && + (!banner.actionUrl.isNullOrBlank() || banner.visibleActions.isNotEmpty()) && + !closedBannerIds.contains(banner.key) + }.sortedByDescending { it.priority } diff --git a/app/src/main/java/one/mixin/android/api/service/ReferralService.kt b/app/src/main/java/one/mixin/android/api/service/ReferralService.kt index cc5df11963..f1d42ed9e2 100644 --- a/app/src/main/java/one/mixin/android/api/service/ReferralService.kt +++ b/app/src/main/java/one/mixin/android/api/service/ReferralService.kt @@ -1,10 +1,17 @@ package one.mixin.android.api.service import one.mixin.android.api.MixinResponse +import one.mixin.android.api.response.WalletHomeBanner import one.mixin.android.api.response.referral.ReferralResponse import retrofit2.http.GET +import retrofit2.http.Query interface ReferralService { @GET("referral") suspend fun referral(): MixinResponse + + @GET("app-banners") + suspend fun walletHomeBanners( + @Query("chain") chains: List? = null, + ): MixinResponse> } diff --git a/app/src/main/java/one/mixin/android/di/AppModule.kt b/app/src/main/java/one/mixin/android/di/AppModule.kt index 2c3e07af43..79661bc904 100644 --- a/app/src/main/java/one/mixin/android/di/AppModule.kt +++ b/app/src/main/java/one/mixin/android/di/AppModule.kt @@ -531,7 +531,7 @@ object AppModule { val sourceRequest = chain.request() val b = sourceRequest.newBuilder() b.addHeader("User-Agent", API_UA) - .addHeader("Accept-Language", Locale.getDefault().language) + .addHeader("Accept-Language", Locale.getDefault().language + "-" + Locale.getDefault().country) .addHeader("Mixin-Device-Id", getStringDeviceId(resolver)) .addHeader(xRequestId, UUID.randomUUID().toString()) val botPublicKey = appContext.defaultSharedPreferences.getString(PREF_ROUTE_BOT_PK, null) diff --git a/app/src/main/java/one/mixin/android/repository/ReferralRepository.kt b/app/src/main/java/one/mixin/android/repository/ReferralRepository.kt index 3faf7e54af..a47cbac2ef 100644 --- a/app/src/main/java/one/mixin/android/repository/ReferralRepository.kt +++ b/app/src/main/java/one/mixin/android/repository/ReferralRepository.kt @@ -4,6 +4,7 @@ import javax.inject.Inject import one.mixin.android.api.referral.ReferralShareInfo import one.mixin.android.api.referral.calculateReferralRebatePercentOrNull import one.mixin.android.api.referral.requestReferralMixinAPI +import one.mixin.android.api.response.WalletHomeBanner import one.mixin.android.api.response.referral.ReferralCode import one.mixin.android.api.response.referral.ReferralResponse import one.mixin.android.api.service.ReferralService @@ -88,6 +89,33 @@ class ReferralRepository requestSession = { userRepository.fetchSessionsSuspend(it) }, ) } + + suspend fun fetchWalletHomeBanners(chains: List = emptyList()): List { + runCatching { + userRepository.getBotPublicKey(REFERRAL_BOT_USER_ID, false) + }.onFailure { + Timber.w(it, "Failed to warm up referral bot session before fetching wallet home banners") + } + + return requestReferralMixinAPI( + invokeNetwork = { referralService.walletHomeBanners(chains.takeIf { it.isNotEmpty() }) }, + successBlock = { response -> response.data.orEmpty() }, + failureBlock = { response -> + Timber.d( + "Fetch wallet home banners failed code=%s message=%s", + response.errorCode, + response.errorDescription, + ) + true + }, + exceptionBlock = { + Timber.w(it, "Fetch wallet home banners failed") + true + }, + requestSession = { userRepository.fetchSessionsSuspend(it) }, + ).orEmpty() + } + } internal fun hasValidReferralMembership(membership: Membership?): Boolean = membership?.isMembership() == true diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt b/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt index 3a2aeeffc9..063a7f6fd6 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt @@ -46,6 +46,7 @@ import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.job.MixinJobManager import one.mixin.android.job.SyncOutputJob import one.mixin.android.repository.AccountRepository +import one.mixin.android.repository.ReferralRepository import one.mixin.android.repository.TokenRepository import one.mixin.android.repository.UserRepository import one.mixin.android.repository.Web3Repository @@ -86,6 +87,7 @@ class Web3ViewModel @Inject constructor( private val userRepository: UserRepository, private val assetRepository: AssetRepository, private val tokenRepository: TokenRepository, + private val referralRepository: ReferralRepository, private val jobManager: MixinJobManager, private val web3Repository: Web3Repository, private val rpc: Rpc, @@ -97,6 +99,8 @@ class Web3ViewModel @Inject constructor( suspend fun findOrSyncApp(appId: String) = userRepository.findOrSyncApp(appId) + suspend fun walletHomeBanners(chains: List) = referralRepository.fetchWalletHomeBanners(chains) + suspend fun findMarketItemByAssetId(assetId: String) = tokenRepository.findMarketItemByAssetId(assetId) fun web3TokensExcludeHidden(walletId: String) = web3Repository.web3TokensExcludeHidden(walletId) diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt index a62f01d7e6..b04b7a7a46 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt @@ -39,6 +39,7 @@ import one.mixin.android.ui.home.web3.trade.perps.PREF_HIDE_SL_GUIDE_UNTIL import one.mixin.android.ui.home.web3.trade.perps.PREF_HIDE_TP_GUIDE_UNTIL import one.mixin.android.ui.setting.diagnosis.DiagnosisFragment import one.mixin.android.ui.wallet.PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED +import one.mixin.android.ui.wallet.PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED import one.mixin.android.ui.wallet.PREF_WALLET_HOME_REFERRAL_CLOSED import one.mixin.android.util.debug.FileLogTree import one.mixin.android.util.viewBinding @@ -143,10 +144,15 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { } resetTpslGuide.setOnClickListener { - resetDebugSharedPreferences() + resetHiddenDebugSharedPreferences() toast(R.string.Reset_TpSl_Guide) } + resetWalletHomeBanners.setOnClickListener { + resetWalletHomeBannerSharedPreferences() + toast(R.string.Reset_Wallet_Home_Banners) + } + deleteWeb3Transactions.setOnClickListener { context?.let { ctx -> alertDialogBuilder() @@ -240,36 +246,21 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { view.loadCaptchaWithoutFallback(captchaType) } - private fun resetDebugSharedPreferences() { + private fun resetHiddenDebugSharedPreferences() { val editor = defaultSharedPreferences.edit() - debugSharedPreferenceKeys().forEach { key -> + hiddenDebugSharedPreferenceKeys(Session.getAccountId()).forEach { key -> editor.remove(key) } editor.apply() } - private fun debugSharedPreferenceKeys(): List = - buildList { - add(PREF_HIDE_TP_GUIDE_UNTIL) - add(PREF_HIDE_SL_GUIDE_UNTIL) - add(TradeFragment.PREF_TRADE_SPOT_GUIDE_SHOWN) - add(TradeFragment.PREF_TRADE_PERPETUAL_GUIDE_SHOWN) - add(Constants.Account.PREF_GLOBAL_MARKET) - add(Constants.Account.PREF_MARKET_TYPE) - add(Constants.Account.PREF_MARKET_ORDER) - add(Constants.Account.PREF_MARKET_TOP_PERCENTAGE) - add(Constants.Account.PREF_HAS_USED_BUY) - add(Constants.Account.PREF_HAS_USED_SWAP) - add(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED) - add(PREF_WALLET_HOME_REFERRAL_CLOSED) - add(PREF_WALLET_HOME_CASHBACK_BANNER_CLOSED) - Session.getAccountId()?.let { accountId -> - add("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}$accountId") - add("${Constants.Account.PREF_TRADE_LIMIT_ORDER_BADGE_DISMISSED}_$accountId") - add("${Constants.Account.PREF_TRADE_PERPETUAL_BADGE_DISMISSED}_$accountId") - add("${Constants.Account.PREF_TRADE_PERPETUAL_ORDER_BADGE_DISMISSED}_$accountId") - } + private fun resetWalletHomeBannerSharedPreferences() { + val editor = defaultSharedPreferences.edit() + walletHomeBannerDebugSharedPreferenceKeys(defaultSharedPreferences.all.keys).forEach { key -> + editor.remove(key) } + editor.apply() + } private fun shareLogsFile() { val dialog = @@ -317,3 +308,34 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { } } } + +private fun hiddenDebugSharedPreferenceKeys(accountId: String?): List = + buildList { + add(PREF_HIDE_TP_GUIDE_UNTIL) + add(PREF_HIDE_SL_GUIDE_UNTIL) + add(TradeFragment.PREF_TRADE_SPOT_GUIDE_SHOWN) + add(TradeFragment.PREF_TRADE_PERPETUAL_GUIDE_SHOWN) + add(Constants.Account.PREF_GLOBAL_MARKET) + add(Constants.Account.PREF_MARKET_TYPE) + add(Constants.Account.PREF_MARKET_ORDER) + add(Constants.Account.PREF_MARKET_TOP_PERCENTAGE) + add(Constants.Account.PREF_HAS_USED_BUY) + add(Constants.Account.PREF_HAS_USED_SWAP) + accountId?.let { + add("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}$it") + add("${Constants.Account.PREF_TRADE_LIMIT_ORDER_BADGE_DISMISSED}_$it") + add("${Constants.Account.PREF_TRADE_PERPETUAL_BADGE_DISMISSED}_$it") + add("${Constants.Account.PREF_TRADE_PERPETUAL_ORDER_BADGE_DISMISSED}_$it") + } + } + +private fun walletHomeBannerDebugSharedPreferenceKeys(existingKeys: Set): Set = + buildSet { + add(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED) + add(PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED) + add(PREF_WALLET_HOME_REFERRAL_CLOSED) + add(PREF_WALLET_HOME_CASHBACK_BANNER_CLOSED) + existingKeys + .filter { it.startsWith("$PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED:") } + .forEach(::add) + } diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletFragment.kt index 4a962a9194..633d7f4d13 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletFragment.kt @@ -484,12 +484,28 @@ class WalletFragment : BaseFragment(R.layout.fragment_wallet) { }?.let { wallet -> jobManager.addJobInBackground(RefreshSingleWalletJob(wallet)) } + refreshWalletHomeBanners() + } + + private fun refreshWalletHomeBanners() { + when (selectedWalletDestination) { + is WalletDestination.Privacy -> { + if (privacyWalletFragment.isAdded) privacyWalletFragment.refreshWalletHomeBanners() + } + is WalletDestination.Classic, + is WalletDestination.Import, + is WalletDestination.Watch, + is WalletDestination.Safe -> { + if (classicWalletFragment.isAdded) classicWalletFragment.refreshWalletHomeBanners() + } + null -> Unit + } } override fun onResume() { super.onResume() jobManager.addJobInBackground(RefreshSafeAccountsJob()) - if (classicWalletFragment.isVisible) classicWalletFragment.update() + update() } private fun closeMenu() { diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt new file mode 100644 index 0000000000..48be72167a --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeBannerActionTarget.kt @@ -0,0 +1,34 @@ +package one.mixin.android.ui.wallet + +import one.mixin.android.Constants +import one.mixin.android.extension.SpotTradeAction +import one.mixin.android.extension.toPerpsTradeAction +import one.mixin.android.extension.toSpotTradeAction + +internal sealed interface WalletHomeBannerActionTarget { + data class SpotTrade(val action: SpotTradeAction) : WalletHomeBannerActionTarget + data class PerpsMarket(val marketId: String) : WalletHomeBannerActionTarget + data object PerpsTab : WalletHomeBannerActionTarget + data object Buy : WalletHomeBannerActionTarget + data class Web(val url: String) : WalletHomeBannerActionTarget +} + +internal fun String.toClassicWalletHomeBannerActionTarget(): WalletHomeBannerActionTarget { + toSpotTradeAction()?.let { return WalletHomeBannerActionTarget.SpotTrade(it) } + toPerpsTradeAction()?.let { action -> + return when (val marketId = action.marketId) { + null -> WalletHomeBannerActionTarget.PerpsTab + else -> WalletHomeBannerActionTarget.PerpsMarket(marketId) + } + } + return if (isBuyAction()) { + WalletHomeBannerActionTarget.Buy + } else { + WalletHomeBannerActionTarget.Web(this) + } +} + +private fun String.isBuyAction(): Boolean = + startsWith(Constants.Scheme.BUY, true) || + startsWith(Constants.Scheme.MIXIN_BUY, true) || + startsWith(Constants.Scheme.HTTPS_BUY, true) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt index e6884c26a7..8f2e7afef0 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt @@ -30,6 +30,10 @@ import kotlinx.coroutines.withContext import one.mixin.android.Constants import one.mixin.android.R import one.mixin.android.RxBus +import one.mixin.android.api.response.WalletHomeBanner +import one.mixin.android.api.response.WalletHomeBannerAction +import one.mixin.android.api.response.syncedWalletHomeClosedBannerIds +import one.mixin.android.api.response.visibleWalletHomeBanners import one.mixin.android.databinding.FragmentPrivacyWalletBinding import one.mixin.android.databinding.ViewWalletFragmentHeaderBinding import one.mixin.android.db.web3.vo.Web3TokenItem @@ -47,6 +51,8 @@ import one.mixin.android.extension.numberFormat2 import one.mixin.android.extension.numberFormat8 import one.mixin.android.extension.openUrl import one.mixin.android.extension.putBoolean +import one.mixin.android.extension.putInt +import one.mixin.android.extension.putStringSet import one.mixin.android.extension.toast import one.mixin.android.extension.withArgs import one.mixin.android.job.MixinJobManager @@ -62,6 +68,8 @@ import one.mixin.android.ui.home.bot.INTERNAL_REFERRAL_ID import one.mixin.android.ui.home.reminder.RecoveryReminderBottomSheetDialogFragment import one.mixin.android.ui.home.web3.Web3ViewModel import one.mixin.android.ui.home.web3.trade.SwapActivity +import one.mixin.android.ui.home.web3.trade.TradeFragment +import one.mixin.android.ui.home.web3.trade.perps.PerpsActivity import one.mixin.android.ui.wallet.home.WalletHomeBuilder import one.mixin.android.ui.wallet.home.WalletHomeCallbacks import one.mixin.android.ui.wallet.home.WalletHomeDataState @@ -133,6 +141,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) private var pendingRawTransactionCount: Int = 0 private var pendingTransactionCount: Int = 0 private var watchAddresses: List = emptyList() + private var dynamicBanners: List = emptyList() private val assetsAdapter by lazy { WalletWeb3TokenAdapter(false) } private var distance = 0 @@ -155,6 +164,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) if (value != field) { field = value walletHomeDataState = WalletHomeDataState.EMPTY + dynamicBanners = emptyList() _walletId.value = value loadWalletHomeCache() } @@ -233,6 +243,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) super.onViewCreated(view, savedInstanceState) Timber.e("onViewCreated called in WalletHomeClassicFragment") refreshBitcoinPrice() + refreshWalletHomeBanners() binding.apply { _headBinding = @@ -487,7 +498,8 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) fiatRate = fiatRate, ) val showAddWalletBanner = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED, false) - val showBanner = showAddWalletBanner + val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds()) + val showBanner = visibleDynamicBanners.isNotEmpty() || showAddWalletBanner val showReferral = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_REFERRAL_CLOSED, false) val currentImportKeyAction = importKeyAction val pendingCount = walletHomePendingTransactionCount(pendingRawTransactionCount, pendingTransactionCount) @@ -521,6 +533,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) watchIndicator = if (isWatchWallet) walletHomeWatchIndicator(watchAddresses) else null, importKeyAction = currentImportKeyAction, showAddWalletBanner = showAddWalletBanner, + dynamicBanners = visibleDynamicBanners, showReferralBanner = showReferral, showImportSafetyFooter = !isLoading, ) @@ -593,6 +606,33 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) renderHome() } + fun refreshWalletHomeBanners() { + if (!isAdded || walletId.isEmpty()) return + lifecycleScope.launch { + val remoteBanners = web3ViewModel.walletHomeBanners(walletHomeBannerChains()) + syncClosedDynamicBannerIds(remoteBanners) + dynamicBanners = remoteBanners + renderHome() + } + } + + private suspend fun walletHomeBannerChains(): List = + web3ViewModel.getAddresses(walletId) + .map { it.chainId } + .filter(String::isNotBlank) + .distinct() + + private fun closedDynamicBannerIds(): Set = + defaultSharedPreferences.getStringSet(dynamicBannerClosedKey(), emptySet()).orEmpty() + + private fun syncClosedDynamicBannerIds(remoteBanners: List) { + val closedBannerIds = closedDynamicBannerIds() + val syncedClosedBannerIds = closedBannerIds.syncedWalletHomeClosedBannerIds(remoteBanners) + if (syncedClosedBannerIds != closedBannerIds) { + defaultSharedPreferences.putStringSet(dynamicBannerClosedKey(), syncedClosedBannerIds) + } + } + private fun loadWalletHomeCache() { if (!isAdded || walletId.isEmpty()) return walletHomeDataState = WalletHomeDataState.EMPTY @@ -619,6 +659,9 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) private fun classicWalletHomeCacheKey(): String = walletHomeCacheKey(WalletHomeType.CLASSIC, walletId) + private fun dynamicBannerClosedKey(): String = + walletHomeDynamicBannerClosedKey(WalletHomeType.CLASSIC, walletId) + private val walletHomeCallbacks = object : WalletHomeCallbacks { override fun onAddWalletClicked() { AddWalletBottomSheetDialogFragment.newInstance().showNow(parentFragmentManager, AddWalletBottomSheetDialogFragment.TAG) @@ -629,6 +672,34 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) renderHome() } + override fun onDynamicBannerClicked(banner: WalletHomeBanner) { + AnalyticsTracker.trackWalletHomeAdBanner( + banner.trackingKey, + AnalyticsTracker.WalletHomeAdBannerSource.BACKGROUND, + ) + banner.actionUrl + ?.takeIf { it.isNotBlank() } + ?.let(::openClassicBannerAction) + } + + override fun onDynamicBannerActionClicked(banner: WalletHomeBanner, action: WalletHomeBannerAction) { + AnalyticsTracker.trackWalletHomeAdBanner( + banner.trackingKey, + AnalyticsTracker.WalletHomeAdBannerSource.BUTTON, + ) + action.action + ?.takeIf { it.isNotBlank() } + ?.let(::openClassicBannerAction) + } + + override fun onDynamicBannerClosed(banner: WalletHomeBanner) { + defaultSharedPreferences.putStringSet( + dynamicBannerClosedKey(), + closedDynamicBannerIds().toMutableSet().apply { add(banner.key) }, + ) + renderHome() + } + override fun onReferralClicked() { lifecycleScope.launch { web3ViewModel.findOrSyncApp(INTERNAL_REFERRAL_ID)?.let { app -> @@ -758,6 +829,60 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) override fun onTopMoverClicked(index: Int) = Unit } + private fun openClassicBannerAction(url: String) { + when (val target = url.toClassicWalletHomeBannerActionTarget()) { + is WalletHomeBannerActionTarget.SpotTrade -> { + val tab = if (target.action.openLimit) TradeFragment.TAB_ADVANCED else TradeFragment.TAB_SIMPLE + AnalyticsTracker.trackTradeStart(TradeWallet.WEB3, TradeSource.WALLET_HOME) + defaultSharedPreferences.putInt("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}$walletId", tab) + SwapActivity.show( + requireActivity(), + target.action.input, + target.action.output, + target.action.amount, + target.action.referral, + inMixin = false, + walletId = walletId, + entrySource = TradeSource.WALLET_HOME, + entryType = if (target.action.openLimit) { + AnalyticsTracker.SpotTradeType.ADVANCED + } else { + AnalyticsTracker.SpotTradeType.SIMPLE + }, + initialTab = tab, + ) + } + is WalletHomeBannerActionTarget.PerpsMarket -> { + PerpsActivity.showDetail( + requireActivity(), + target.marketId, + "", + "", + "", + AnalyticsTracker.PerpsSource.WALLET_HOME, + ) + } + WalletHomeBannerActionTarget.PerpsTab -> { + AnalyticsTracker.trackTradeStart(TradeWallet.WEB3, TradeSource.WALLET_HOME) + defaultSharedPreferences.putInt("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}$walletId", TradeFragment.TAB_PERPETUAL) + SwapActivity.show( + requireActivity(), + inMixin = false, + walletId = walletId, + entrySource = TradeSource.WALLET_HOME, + entryType = AnalyticsTracker.SpotTradeType.PERPETUAL, + initialTab = TradeFragment.TAB_PERPETUAL, + ) + } + WalletHomeBannerActionTarget.Buy -> { + WalletActivity.showBuy(requireActivity(), true, null, null, walletId) + } + is WalletHomeBannerActionTarget.Web -> { + WebActivity.show(requireActivity(), target.url, null) + } + } + } + private fun showPendingTransactions() { if (walletId.isNotEmpty()) { WalletActivity.show(requireActivity(), WalletActivity.Destination.AllWeb3Transactions(walletId = walletId), pendingType = true) @@ -797,6 +922,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) lifecycleScope.launch { refreshWalletHomeMetadata(walletId) } + refreshWalletHomeBanners() } refreshJob = PendingTransactionRefreshHelper.startRefreshData( fragment = this, @@ -815,6 +941,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) override fun onHiddenChanged(hidden: Boolean) { if (!hidden) { jobManager.addJobInBackground(RefreshSingleWalletJob(Web3Signer.currentWalletId)) + refreshWalletHomeBanners() } } diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt index 5a850f6b0a..280c0d2f3e 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt @@ -1,4 +1,12 @@ package one.mixin.android.ui.wallet +import one.mixin.android.ui.wallet.home.WalletHomeType + internal const val PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED = "pref_wallet_home_add_wallet_banner_closed" +internal const val PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED = "pref_wallet_home_dynamic_banner_closed" internal const val PREF_WALLET_HOME_REFERRAL_CLOSED = "pref_wallet_home_referral_closed" + +internal fun walletHomeDynamicBannerClosedKey( + walletType: WalletHomeType, + walletId: String, +) = "$PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED:${walletType.name}:$walletId" diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt index 64d4656637..780f9de4cf 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt @@ -32,6 +32,10 @@ import one.mixin.android.Constants.Account.PREF_HAS_USED_SWAP import one.mixin.android.R import one.mixin.android.RxBus import one.mixin.android.api.handleMixinResponse +import one.mixin.android.api.response.WalletHomeBanner +import one.mixin.android.api.response.WalletHomeBannerAction +import one.mixin.android.api.response.syncedWalletHomeClosedBannerIds +import one.mixin.android.api.response.visibleWalletHomeBanners import one.mixin.android.databinding.FragmentPrivacyWalletBinding import one.mixin.android.databinding.ViewWalletFragmentHeaderBinding import one.mixin.android.event.BadgeEvent @@ -44,9 +48,11 @@ import one.mixin.android.extension.mainThread import one.mixin.android.extension.navTo import one.mixin.android.extension.numberFormat2 import one.mixin.android.extension.numberFormat8 +import one.mixin.android.extension.openAsUrlOrWeb import one.mixin.android.extension.openUrl import one.mixin.android.extension.toast import one.mixin.android.extension.putBoolean +import one.mixin.android.extension.putStringSet import one.mixin.android.job.MixinJobManager import one.mixin.android.job.RefreshSnapshotsJob import one.mixin.android.job.RefreshTokensJob @@ -131,6 +137,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) private var positions: List = emptyList() private var topMovers: List = emptyList() private var pendingDisplays: List = emptyList() + private var dynamicBanners: List = emptyList() private val assetsAdapter by lazy { WalletAssetAdapter(false) } private val perpetualViewModel by viewModels() private var walletHomeDataState = WalletHomeDataState.EMPTY @@ -190,6 +197,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) Timber.e("onViewCreated called in WalletHomePrivacyFragment") _walletId.value = Session.getAccountId().orEmpty() refreshBitcoinPrice() + refreshWalletHomeBanners() binding.apply { _headBinding = @@ -393,7 +401,8 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) fiatRate = fiatRate, ) val showAddWalletBanner = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED, false) - val showBanner = showAddWalletBanner + val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds()) + val showBanner = visibleDynamicBanners.isNotEmpty() || showAddWalletBanner val showReferral = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_REFERRAL_CLOSED, false) val cards = WalletHomeBuilder.build( walletType = WalletHomeType.PRIVACY, @@ -426,6 +435,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) pendingIndicator = pendingDisplays.toWalletHomePendingIndicator(), quoteColorReversed = defaultSharedPreferences.getBoolean(Constants.Account.PREF_QUOTE_COLOR, false), showAddWalletBanner = showAddWalletBanner, + dynamicBanners = visibleDynamicBanners, showReferralBanner = showReferral, showBuyBadge = defaultSharedPreferences.getBoolean(PREF_HAS_USED_BUY, true), showSwapBadge = defaultSharedPreferences.getBoolean(PREF_HAS_USED_SWAP, true), @@ -459,10 +469,32 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) ?.takeIf { it > BigDecimal.ZERO } ?: bitcoinPriceUsd + fun refreshWalletHomeBanners() { + lifecycleScope.launch { + val remoteBanners = walletViewModel.walletHomeBanners() + syncClosedDynamicBannerIds(remoteBanners) + dynamicBanners = remoteBanners + renderHome() + } + } + + private fun closedDynamicBannerIds(): Set = + defaultSharedPreferences.getStringSet(dynamicBannerClosedKey(), emptySet()).orEmpty() + + private fun syncClosedDynamicBannerIds(remoteBanners: List) { + val closedBannerIds = closedDynamicBannerIds() + val syncedClosedBannerIds = closedBannerIds.syncedWalletHomeClosedBannerIds(remoteBanners) + if (syncedClosedBannerIds != closedBannerIds) { + defaultSharedPreferences.putStringSet(dynamicBannerClosedKey(), syncedClosedBannerIds) + } + } private fun privacyWalletHomeCacheKey(): String = walletHomeCacheKey(WalletHomeType.PRIVACY, Session.getAccountId().orEmpty()) + private fun dynamicBannerClosedKey(): String = + walletHomeDynamicBannerClosedKey(WalletHomeType.PRIVACY, Session.getAccountId().orEmpty()) + private fun List.toWalletHomePositionSummary(): WalletHomePositionSummary? { if (isEmpty()) return null val totalMargin = positionMarginUsdTotal() @@ -488,7 +520,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) private val walletHomeCallbacks = object : WalletHomeCallbacks { override fun onAddWalletClicked() { - AddWalletBottomSheetDialogFragment.newInstance().showNow(parentFragmentManager, AddWalletBottomSheetDialogFragment.TAG) + showAddWalletDialog() } override fun onBannerClosed() { @@ -496,6 +528,34 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) renderHome() } + override fun onDynamicBannerClicked(banner: WalletHomeBanner) { + AnalyticsTracker.trackWalletHomeAdBanner( + banner.trackingKey, + AnalyticsTracker.WalletHomeAdBannerSource.BACKGROUND, + ) + banner.actionUrl + ?.takeIf { it.isNotBlank() } + ?.openAsUrlOrWeb(requireActivity(), null, parentFragmentManager, lifecycleScope) + } + + override fun onDynamicBannerActionClicked(banner: WalletHomeBanner, action: WalletHomeBannerAction) { + AnalyticsTracker.trackWalletHomeAdBanner( + banner.trackingKey, + AnalyticsTracker.WalletHomeAdBannerSource.BUTTON, + ) + action.action + ?.takeIf { it.isNotBlank() } + ?.openAsUrlOrWeb(requireActivity(), null, parentFragmentManager, lifecycleScope) + } + + override fun onDynamicBannerClosed(banner: WalletHomeBanner) { + defaultSharedPreferences.putStringSet( + dynamicBannerClosedKey(), + closedDynamicBannerIds().toMutableSet().apply { add(banner.key) }, + ) + renderHome() + } + override fun onReferralClicked() { lifecycleScope.launch { walletViewModel.findOrSyncApp(INTERNAL_REFERRAL_ID)?.let { app -> @@ -666,6 +726,10 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) } } + private fun showAddWalletDialog() { + AddWalletBottomSheetDialogFragment.newInstance().showNow(parentFragmentManager, AddWalletBottomSheetDialogFragment.TAG) + } + override fun onResume() { super.onResume() _walletId.value = Session.getAccountId().orEmpty() @@ -686,6 +750,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) jobManager.addJobInBackground(RefreshSnapshotsJob()) jobManager.addJobInBackground(SyncOutputJob()) refreshAllPendingDeposit() + refreshWalletHomeBanners() } } diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt index a98474f311..73002de9d1 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt @@ -44,6 +44,7 @@ import one.mixin.android.job.RefreshTokensJob import one.mixin.android.job.RefreshTopAssetsJob import one.mixin.android.job.RefreshUserJob import one.mixin.android.repository.AccountRepository +import one.mixin.android.repository.ReferralRepository import one.mixin.android.repository.TokenRepository import one.mixin.android.repository.UserRepository import one.mixin.android.repository.Web3Repository @@ -71,6 +72,7 @@ internal constructor( private val accountRepository: AccountRepository, private val web3Repository: Web3Repository, private val tokenRepository: TokenRepository, + private val referralRepository: ReferralRepository, private val assetRepository: AssetRepository, private val jobManager: MixinJobManager, private val pinCipher: PinCipher, @@ -332,6 +334,8 @@ internal constructor( suspend fun profile(): MixinResponse = tokenRepository.profile() + suspend fun walletHomeBanners() = referralRepository.fetchWalletHomeBanners() + suspend fun fetchSessionsSuspend(ids: List) = userRepository.fetchSessionsSuspend(ids) suspend fun findBondBotUrl() = userRepository.findOrSyncApp(MIXIN_BOND_USER_ID) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt index 13709f9c3b..cce87436ff 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt @@ -3,6 +3,8 @@ package one.mixin.android.ui.wallet.home import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.api.response.perps.PerpsMarket import one.mixin.android.R +import one.mixin.android.api.response.WalletHomeBanner +import one.mixin.android.api.response.WalletHomeBannerAction import one.mixin.android.db.web3.vo.Web3TokenItem import one.mixin.android.db.web3.vo.Web3TransactionItem import one.mixin.android.extension.numberFormat8 @@ -38,6 +40,7 @@ data class WalletHomeState( val hideActions: Boolean = false, val quoteColorReversed: Boolean = false, val showAddWalletBanner: Boolean = false, + val dynamicBanners: List = emptyList(), val showReferralBanner: Boolean = false, val showBuyBadge: Boolean = false, val showSwapBadge: Boolean = false, @@ -61,6 +64,9 @@ data class WalletHomeBalanceSnapshot( interface WalletHomeCallbacks { fun onAddWalletClicked() fun onBannerClosed() + fun onDynamicBannerClicked(banner: WalletHomeBanner) = Unit + fun onDynamicBannerActionClicked(banner: WalletHomeBanner, action: WalletHomeBannerAction) = Unit + fun onDynamicBannerClosed(banner: WalletHomeBanner) = Unit fun onReferralClicked() fun onReferralClosed() fun onSupportClicked() diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 32e9a98e03..6e16e7f858 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -36,10 +36,14 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import one.mixin.android.R +import one.mixin.android.api.response.WalletHomeBanner +import one.mixin.android.api.response.WalletHomeBannerAction +import one.mixin.android.compose.CoilImageCompat import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.ui.wallet.alert.components.cardBackground import one.mixin.android.ui.wallet.home.WalletHomeCallbacks @@ -51,9 +55,10 @@ internal fun BannerPager( state: WalletHomeState, callbacks: WalletHomeCallbacks, ) { - val pages = remember(state.showAddWalletBanner) { + val pages = remember(state.showAddWalletBanner, state.dynamicBanners) { buildList { - if (state.showAddWalletBanner) add(WalletHomeBannerPage.ADD_WALLET) + addAll(state.dynamicBanners.map(WalletHomeBannerPage::Dynamic)) + if (state.showAddWalletBanner) add(WalletHomeBannerPage.AddWallet) } } if (pages.isEmpty()) return @@ -70,7 +75,7 @@ internal fun BannerPager( .wrapContentHeight() .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor), ) { - Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp)) { + Column(modifier = Modifier.padding(horizontal = 20.dp)) { HorizontalPager( state = pagerState, modifier = Modifier @@ -78,14 +83,19 @@ internal fun BannerPager( .wrapContentHeight(), verticalAlignment = Alignment.CenterVertically, ) { page -> - when (pages[page]) { - WalletHomeBannerPage.ADD_WALLET -> BannerCard( + when (val bannerPage = pages[page]) { + WalletHomeBannerPage.AddWallet -> BannerCard( iconRes = R.drawable.ic_wallet_home_add, titleRes = R.string.wallet_home_add_wallet_banner_title, descriptionRes = null, ctaRes = R.string.add_wallet, onClick = callbacks::onAddWalletClicked, ) + is WalletHomeBannerPage.Dynamic -> DynamicBannerCard( + banner = bannerPage.banner, + onClick = callbacks::onDynamicBannerClicked, + onActionClick = callbacks::onDynamicBannerActionClicked, + ) } } } @@ -98,8 +108,9 @@ internal fun BannerPager( .padding(top = 16.dp, end = 16.dp) .size(12.dp) .clickable { - when (pages.getOrNull(pagerState.currentPage)) { - WalletHomeBannerPage.ADD_WALLET -> callbacks.onBannerClosed() + when (val currentPage = pages.getOrNull(pagerState.currentPage)) { + WalletHomeBannerPage.AddWallet -> callbacks.onBannerClosed() + is WalletHomeBannerPage.Dynamic -> callbacks.onDynamicBannerClosed(currentPage.banner) null -> Unit } }, @@ -141,7 +152,7 @@ private fun BannerCard( Row( modifier = Modifier .fillMaxWidth() - .padding(end = 22.dp), + .padding(top = 20.dp, end = 22.dp, bottom = 20.dp), verticalAlignment = Alignment.Top, ) { Image( @@ -167,7 +178,82 @@ private fun BannerCard( ) } Spacer(modifier = Modifier.height(12.dp)) - BannerAction(textRes = ctaRes, onClick = onClick) + BannerAction(text = stringResource(ctaRes), onClick = onClick) + } + } +} + +@Composable +private fun DynamicBannerCard( + banner: WalletHomeBanner, + onClick: (WalletHomeBanner) -> Unit, + onActionClick: (WalletHomeBanner, WalletHomeBannerAction) -> Unit, +) { + val actions = banner.visibleActions + val iconShape = if (banner.hasButtonStyle) CircleShape else RoundedCornerShape(8.dp) + val bottomPadding = if (banner.hasButtonStyle) 20.dp else 22.dp + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 22.dp, end = 22.dp, bottom = bottomPadding) + .clickable { onClick(banner) }, + verticalAlignment = Alignment.Top, + ) { + val iconUrl = banner.iconUrl?.takeIf { it.isNotBlank() } + if (iconUrl != null) { + CoilImageCompat( + model = iconUrl, + placeholder = R.drawable.ic_deafult, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(42.dp) + .clip(iconShape), + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_deafult), + contentDescription = null, + modifier = Modifier + .size(42.dp) + .clip(iconShape), + ) + } + Spacer(modifier = Modifier.width(14.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = banner.title.orEmpty(), + color = MixinAppTheme.colors.textMinor, + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.W500, + ) + if (actions.isEmpty()) { + banner.description?.takeIf { it.isNotBlank() }?.let { description -> + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = description, + color = MixinAppTheme.colors.textAssist, + fontSize = 14.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.W400, + ) + } + } else { + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + actions.forEach { action -> + BannerAction( + text = action.label.orEmpty(), + onClick = { onActionClick(banner, action) }, + modifier = Modifier.weight(1f, fill = false), + fontWeight = FontWeight.W400, + ) + } + } + } } } } @@ -246,7 +332,7 @@ internal fun ReferralBannerCard(callbacks: WalletHomeCallbacks) { ) Spacer(modifier = Modifier.height(16.dp)) BannerAction( - textRes = R.string.Learn_More, + text = stringResource(R.string.Learn_More), primary = true, onClick = callbacks::onReferralClicked, modifier = Modifier @@ -264,7 +350,7 @@ internal fun ReferralBannerCard(callbacks: WalletHomeCallbacks) { @Composable private fun BannerAction( - textRes: Int, + text: String, primary: Boolean = false, onClick: (() -> Unit)? = null, modifier: Modifier = Modifier, @@ -283,16 +369,19 @@ private fun BannerAction( contentAlignment = Alignment.Center, ) { Text( - text = stringResource(textRes), + text = text, color = if (primary) Color.White else MixinAppTheme.colors.accent, fontSize = 14.sp, fontWeight = fontWeight, lineHeight = lineHeight?.sp ?: 18.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, ) } } -private enum class WalletHomeBannerPage { - ADD_WALLET, +private sealed class WalletHomeBannerPage { + data object AddWallet : WalletHomeBannerPage() + data class Dynamic(val banner: WalletHomeBanner) : WalletHomeBannerPage() } diff --git a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt index a7a118ee9e..50eb38397e 100644 --- a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt +++ b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt @@ -151,6 +151,19 @@ object AnalyticsTracker { } } + fun trackWalletHomeAdBanner(trackingKey: String?, source: String) { + val key = trackingKey?.takeIf { it.isNotBlank() } ?: return + logEvent("wallet_home_ad_banner") { + putString("tracking_key", key) + putString("source", source) + } + } + + object WalletHomeAdBannerSource { + const val BACKGROUND = "wallet_home_ad_banner_background" + const val BUTTON = "wallet_home_ad_banner_button" + } + object AssetSource { const val WALLET_HOME = "wallet_home" const val TOKEN_LIST = "token_list" diff --git a/app/src/main/res/drawable/ic_deafult.xml b/app/src/main/res/drawable/ic_deafult.xml new file mode 100644 index 0000000000..49120d3e2a --- /dev/null +++ b/app/src/main/res/drawable/ic_deafult.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_log_debug.xml b/app/src/main/res/layout/fragment_log_debug.xml index 75bbc02edc..280fd15734 100644 --- a/app/src/main/res/layout/fragment_log_debug.xml +++ b/app/src/main/res/layout/fragment_log_debug.xml @@ -189,6 +189,18 @@ android:foreground="?android:attr/selectableItemBackground" android:textSize="16sp" /> + + 所有资产 隐藏的资产不计入统计 重置隐藏偏好 + 重置钱包首页 Banner 删除 Web3 交易数据 确定要删除所有 Web3 交易数据吗?此操作不可恢复。删除后系统将重新同步数据。 预览手机号提醒弹窗 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8909430af1..8c00267e43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2037,6 +2037,7 @@ Total Balance Hidden assets are not included in the statistics. Reset hidden preferences + Reset wallet home banners Delete Web3 Transaction Data Are you sure you want to delete all Web3 transaction data? This action cannot be undone. The data will be re-synchronized after deletion. Preview verify mobile reminder diff --git a/app/src/test/java/one/mixin/android/api/response/WalletHomeBannerTest.kt b/app/src/test/java/one/mixin/android/api/response/WalletHomeBannerTest.kt new file mode 100644 index 0000000000..8a65c4a4e0 --- /dev/null +++ b/app/src/test/java/one/mixin/android/api/response/WalletHomeBannerTest.kt @@ -0,0 +1,54 @@ +package one.mixin.android.api.response + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class WalletHomeBannerTest { + @Test + fun bannerWithActionsUsesButtonStyle() { + assertTrue( + WalletHomeBanner( + actions = listOf(WalletHomeBannerAction(label = "Claim", action = "mixin://buy")), + ).hasButtonStyle, + ) + } + + @Test + fun bannerWithoutActionsUsesNoButtonStyle() { + assertFalse(WalletHomeBanner().hasButtonStyle) + assertFalse( + WalletHomeBanner( + actions = listOf(WalletHomeBannerAction(label = "Claim", action = null)), + ).hasButtonStyle, + ) + } + + @Test + fun visibleActionsOnlyUsesFirstValidAction() { + val banner = WalletHomeBanner( + actions = listOf( + WalletHomeBannerAction(label = null, action = "mixin://skip"), + WalletHomeBannerAction(label = "First", action = "mixin://first"), + WalletHomeBannerAction(label = "Second", action = "mixin://second"), + ), + ) + + assertEquals(listOf("First"), banner.visibleActions.map { it.label }) + } + + @Test + fun syncClosedBannerIdsKeepsOnlyRemoteBannerKeys() { + val remoteBanners = listOf( + WalletHomeBanner(bannerId = "remote-1"), + WalletHomeBanner(bannerId = "remote-2"), + ) + + assertEquals( + setOf("remote-1"), + setOf("remote-1", "removed").syncedWalletHomeClosedBannerIds(remoteBanners), + ) + } + +} diff --git a/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt b/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt index be14101de9..4a654ebf91 100644 --- a/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt +++ b/app/src/test/java/one/mixin/android/ui/conversation/PerpsTradeActionTest.kt @@ -4,6 +4,8 @@ import one.mixin.android.extension.PerpsTradeAction import one.mixin.android.extension.SpotTradeAction import one.mixin.android.extension.toPerpsTradeAction import one.mixin.android.extension.toSpotTradeAction +import one.mixin.android.ui.wallet.WalletHomeBannerActionTarget +import one.mixin.android.ui.wallet.toClassicWalletHomeBannerActionTarget import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -85,4 +87,47 @@ class PerpsTradeActionTest { assertNull(action.toPerpsTradeAction()) assertNull(action.toSpotTradeAction()) } + + @Test + fun classicBannerActionParsesSpotTradeTarget() { + val action = "https://mixin.one/swap?input=43d61dcd-e413-450d-80b8-101d5e903357&output=c6d0c728-2624-429b-8e0d-d9d19b6592fa&amount=1.2&referral=7000" + + val target = action.toClassicWalletHomeBannerActionTarget() + + assertEquals( + WalletHomeBannerActionTarget.SpotTrade( + SpotTradeAction( + input = "43d61dcd-e413-450d-80b8-101d5e903357", + output = "c6d0c728-2624-429b-8e0d-d9d19b6592fa", + amount = "1.2", + referral = "7000", + openLimit = false, + ), + ), + target, + ) + } + + @Test + fun classicBannerActionParsesPerpsAndBuyTargets() { + assertEquals( + WalletHomeBannerActionTarget.PerpsMarket("e015f42e-b0ff-38e7-87b1-7e8d46fea119"), + "https://mixin.one/trade?type=perps&market=e015f42e-b0ff-38e7-87b1-7e8d46fea119".toClassicWalletHomeBannerActionTarget(), + ) + assertEquals( + WalletHomeBannerActionTarget.PerpsTab, + "https://mixin.one/trade?type=perps".toClassicWalletHomeBannerActionTarget(), + ) + assertEquals( + WalletHomeBannerActionTarget.Buy, + "https://mixin.one/buy".toClassicWalletHomeBannerActionTarget(), + ) + } + + @Test + fun classicBannerActionFallsBackToWebTarget() { + val url = "https://mixin.one/users/41d16c28-0c3a-493d-a2b4-b57875371abf" + + assertEquals(WalletHomeBannerActionTarget.Web(url), url.toClassicWalletHomeBannerActionTarget()) + } } diff --git a/app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt b/app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt new file mode 100644 index 0000000000..7e012bf1bf --- /dev/null +++ b/app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt @@ -0,0 +1,19 @@ +package one.mixin.android.ui.wallet + +import one.mixin.android.ui.wallet.home.WalletHomeType +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class WalletHomePreferencesTest { + @Test + fun dynamicBannerClosedKeyIsScopedByWalletTypeAndWalletId() { + val classicWalletA = walletHomeDynamicBannerClosedKey(WalletHomeType.CLASSIC, "wallet-a") + val classicWalletB = walletHomeDynamicBannerClosedKey(WalletHomeType.CLASSIC, "wallet-b") + val privacyWalletA = walletHomeDynamicBannerClosedKey(WalletHomeType.PRIVACY, "wallet-a") + + assertEquals("pref_wallet_home_dynamic_banner_closed:CLASSIC:wallet-a", classicWalletA) + assertNotEquals(classicWalletA, classicWalletB) + assertNotEquals(classicWalletA, privacyWalletA) + } +} diff --git a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt index 673a7d63ac..eb6c1c4e97 100644 --- a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt +++ b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt @@ -1,5 +1,8 @@ package one.mixin.android.ui.wallet.home +import one.mixin.android.api.response.WalletHomeBanner +import one.mixin.android.api.response.WalletHomeBannerAction +import one.mixin.android.api.response.visibleWalletHomeBanners import org.junit.Assert.assertEquals import org.junit.Test @@ -74,4 +77,158 @@ class WalletHomeBuilderTest { cards, ) } + + @Test + fun visibleDynamicBannerShowsBannerCard() { + val banners = listOf( + WalletHomeBanner( + bannerId = "banner-1", + title = "Promo", + actionUrl = "https://example.com", + status = "active", + ), + ).visibleWalletHomeBanners(closedBannerIds = emptySet()) + + val cards = WalletHomeBuilder.build( + walletType = WalletHomeType.PRIVACY, + hasAssetValue = true, + showBanner = banners.isNotEmpty(), + showReferral = false, + hasPositions = false, + hasTopMovers = false, + hasTransactions = false, + ) + + assertEquals( + listOf( + WalletHomeCardType.BALANCE, + WalletHomeCardType.BANNER, + WalletHomeCardType.TOKENS, + WalletHomeCardType.SUPPORT, + ), + cards, + ) + } + + @Test + fun classicWalletShowsDynamicBannerCard() { + val cards = WalletHomeBuilder.build( + walletType = WalletHomeType.CLASSIC, + hasAssetValue = true, + showBanner = true, + showReferral = false, + hasPositions = false, + hasTopMovers = false, + hasTransactions = false, + ) + + assertEquals( + listOf( + WalletHomeCardType.BALANCE, + WalletHomeCardType.BANNER, + WalletHomeCardType.TOKENS, + WalletHomeCardType.SUPPORT, + ), + cards, + ) + } + + @Test + fun closedDynamicBannerDoesNotShowBannerCard() { + val banners = listOf( + WalletHomeBanner( + bannerId = "banner-1", + title = "Promo", + actionUrl = "https://example.com", + status = "active", + ), + ).visibleWalletHomeBanners(closedBannerIds = setOf("banner-1")) + + val cards = WalletHomeBuilder.build( + walletType = WalletHomeType.PRIVACY, + hasAssetValue = true, + showBanner = banners.isNotEmpty(), + showReferral = false, + hasPositions = false, + hasTopMovers = false, + hasTransactions = false, + ) + + assertEquals( + listOf( + WalletHomeCardType.BALANCE, + WalletHomeCardType.TOKENS, + WalletHomeCardType.SUPPORT, + ), + cards, + ) + } + + @Test + fun visibleDynamicBannersPreferPriorityAndIgnoreInactive() { + val banners = listOf( + WalletHomeBanner( + bannerId = "low", + title = "Low", + actionUrl = "https://example.com/low", + status = "active", + priority = 1, + ), + WalletHomeBanner( + bannerId = "inactive", + title = "Inactive", + actionUrl = "https://example.com/inactive", + status = "inactive", + priority = 100, + ), + WalletHomeBanner( + bannerId = "closed", + title = "Closed", + actionUrl = "https://example.com/closed", + status = "active", + priority = 90, + ), + WalletHomeBanner( + bannerId = "high", + title = "High", + actionUrl = "https://example.com/high", + status = "active", + priority = 10, + ), + ).visibleWalletHomeBanners(closedBannerIds = setOf("closed")) + + assertEquals(listOf("high", "low"), banners.map { it.bannerId }) + } + + @Test + fun dynamicBannerWithActionsShowsBannerCard() { + val banners = listOf( + WalletHomeBanner( + bannerId = "banner-1", + title = "Promo", + status = "active", + actions = listOf(WalletHomeBannerAction(label = "Claim", action = "mixin://buy")), + ), + ).visibleWalletHomeBanners(closedBannerIds = emptySet()) + + val cards = WalletHomeBuilder.build( + walletType = WalletHomeType.PRIVACY, + hasAssetValue = true, + showBanner = banners.isNotEmpty(), + showReferral = false, + hasPositions = false, + hasTopMovers = false, + hasTransactions = false, + ) + + assertEquals( + listOf( + WalletHomeCardType.BALANCE, + WalletHomeCardType.BANNER, + WalletHomeCardType.TOKENS, + WalletHomeCardType.SUPPORT, + ), + cards, + ) + } } From 6a74fc44e59f36b50675c059c142487fee6e0d71 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 15:45:31 +0800 Subject: [PATCH 02/12] fix(wallet): persist home banner dismissal globally --- .../android/ui/setting/LogAndDebugFragment.kt | 11 +++-- .../ui/wallet/WalletHomeClassicFragment.kt | 42 ++++++++----------- .../ui/wallet/WalletHomePreferences.kt | 32 +++++++++++--- .../ui/wallet/WalletHomePrivacyFragment.kt | 28 ++++++------- .../ui/wallet/home/components/BannerCards.kt | 7 ++-- .../ui/wallet/WalletHomePreferencesTest.kt | 15 +++---- 6 files changed, 73 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt index b04b7a7a46..1d6b0e9f87 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt @@ -19,6 +19,7 @@ import one.mixin.android.R import one.mixin.android.databinding.FragmentLogDebugBinding import one.mixin.android.databinding.ViewCaptchaPreviewBottomBinding import one.mixin.android.db.DatabaseMonitor +import one.mixin.android.db.property.PropertyHelper.deleteKeyValue import one.mixin.android.db.property.PropertyHelper.findValueByKey import one.mixin.android.db.property.PropertyHelper.updateKeyValue import one.mixin.android.extension.alertDialogBuilder @@ -149,8 +150,10 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { } resetWalletHomeBanners.setOnClickListener { - resetWalletHomeBannerSharedPreferences() - toast(R.string.Reset_Wallet_Home_Banners) + lifecycleScope.launch { + resetWalletHomeBannerLocalState() + toast(R.string.Reset_Wallet_Home_Banners) + } } deleteWeb3Transactions.setOnClickListener { @@ -254,12 +257,13 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { editor.apply() } - private fun resetWalletHomeBannerSharedPreferences() { + private suspend fun resetWalletHomeBannerLocalState() { val editor = defaultSharedPreferences.edit() walletHomeBannerDebugSharedPreferenceKeys(defaultSharedPreferences.all.keys).forEach { key -> editor.remove(key) } editor.apply() + deleteKeyValue(PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED) } private fun shareLogsFile() { @@ -335,6 +339,7 @@ private fun walletHomeBannerDebugSharedPreferenceKeys(existingKeys: Set) add(PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED) add(PREF_WALLET_HOME_REFERRAL_CLOSED) add(PREF_WALLET_HOME_CASHBACK_BANNER_CLOSED) + add(Constants.Account.PREF_HAS_USED_ADD_WALLET) existingKeys .filter { it.startsWith("$PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED:") } .forEach(::add) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt index 8f2e7afef0..efba4ff8f3 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt @@ -52,7 +52,6 @@ import one.mixin.android.extension.numberFormat8 import one.mixin.android.extension.openUrl import one.mixin.android.extension.putBoolean import one.mixin.android.extension.putInt -import one.mixin.android.extension.putStringSet import one.mixin.android.extension.toast import one.mixin.android.extension.withArgs import one.mixin.android.job.MixinJobManager @@ -142,6 +141,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) private var pendingTransactionCount: Int = 0 private var watchAddresses: List = emptyList() private var dynamicBanners: List = emptyList() + private var closedDynamicBannerIds: Set = emptySet() private val assetsAdapter by lazy { WalletWeb3TokenAdapter(false) } private var distance = 0 @@ -498,7 +498,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) fiatRate = fiatRate, ) val showAddWalletBanner = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED, false) - val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds()) + val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds) val showBanner = visibleDynamicBanners.isNotEmpty() || showAddWalletBanner val showReferral = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_REFERRAL_CLOSED, false) val currentImportKeyAction = importKeyAction @@ -622,15 +622,13 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) .filter(String::isNotBlank) .distinct() - private fun closedDynamicBannerIds(): Set = - defaultSharedPreferences.getStringSet(dynamicBannerClosedKey(), emptySet()).orEmpty() - - private fun syncClosedDynamicBannerIds(remoteBanners: List) { - val closedBannerIds = closedDynamicBannerIds() + private suspend fun syncClosedDynamicBannerIds(remoteBanners: List) { + val closedBannerIds = findWalletHomeDynamicBannerClosedIds() val syncedClosedBannerIds = closedBannerIds.syncedWalletHomeClosedBannerIds(remoteBanners) if (syncedClosedBannerIds != closedBannerIds) { - defaultSharedPreferences.putStringSet(dynamicBannerClosedKey(), syncedClosedBannerIds) + updateWalletHomeDynamicBannerClosedIds(syncedClosedBannerIds) } + closedDynamicBannerIds = syncedClosedBannerIds } private fun loadWalletHomeCache() { @@ -659,9 +657,6 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) private fun classicWalletHomeCacheKey(): String = walletHomeCacheKey(WalletHomeType.CLASSIC, walletId) - private fun dynamicBannerClosedKey(): String = - walletHomeDynamicBannerClosedKey(WalletHomeType.CLASSIC, walletId) - private val walletHomeCallbacks = object : WalletHomeCallbacks { override fun onAddWalletClicked() { AddWalletBottomSheetDialogFragment.newInstance().showNow(parentFragmentManager, AddWalletBottomSheetDialogFragment.TAG) @@ -693,11 +688,12 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) } override fun onDynamicBannerClosed(banner: WalletHomeBanner) { - defaultSharedPreferences.putStringSet( - dynamicBannerClosedKey(), - closedDynamicBannerIds().toMutableSet().apply { add(banner.key) }, - ) - renderHome() + lifecycleScope.launch { + val closedIds = closedDynamicBannerIds.toMutableSet().apply { add(banner.key) } + updateWalletHomeDynamicBannerClosedIds(closedIds) + closedDynamicBannerIds = closedIds + renderHome() + } } override fun onReferralClicked() { @@ -833,16 +829,14 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) when (val target = url.toClassicWalletHomeBannerActionTarget()) { is WalletHomeBannerActionTarget.SpotTrade -> { val tab = if (target.action.openLimit) TradeFragment.TAB_ADVANCED else TradeFragment.TAB_SIMPLE - AnalyticsTracker.trackTradeStart(TradeWallet.WEB3, TradeSource.WALLET_HOME) - defaultSharedPreferences.putInt("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}$walletId", tab) + AnalyticsTracker.trackTradeStart(TradeWallet.MAIN, TradeSource.WALLET_HOME) + defaultSharedPreferences.putInt("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}${Session.getAccountId().orEmpty()}", tab) SwapActivity.show( requireActivity(), target.action.input, target.action.output, target.action.amount, target.action.referral, - inMixin = false, - walletId = walletId, entrySource = TradeSource.WALLET_HOME, entryType = if (target.action.openLimit) { AnalyticsTracker.SpotTradeType.ADVANCED @@ -863,19 +857,17 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) ) } WalletHomeBannerActionTarget.PerpsTab -> { - AnalyticsTracker.trackTradeStart(TradeWallet.WEB3, TradeSource.WALLET_HOME) - defaultSharedPreferences.putInt("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}$walletId", TradeFragment.TAB_PERPETUAL) + AnalyticsTracker.trackTradeStart(TradeWallet.MAIN, TradeSource.WALLET_HOME) + defaultSharedPreferences.putInt("${TradeFragment.PREF_TRADE_SELECTED_TAB_PREFIX}${Session.getAccountId().orEmpty()}", TradeFragment.TAB_PERPETUAL) SwapActivity.show( requireActivity(), - inMixin = false, - walletId = walletId, entrySource = TradeSource.WALLET_HOME, entryType = AnalyticsTracker.SpotTradeType.PERPETUAL, initialTab = TradeFragment.TAB_PERPETUAL, ) } WalletHomeBannerActionTarget.Buy -> { - WalletActivity.showBuy(requireActivity(), true, null, null, walletId) + WalletActivity.showBuy(requireActivity(), false, null, null) } is WalletHomeBannerActionTarget.Web -> { WebActivity.show(requireActivity(), target.url, null) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt index 280c0d2f3e..52a216dbd4 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePreferences.kt @@ -1,12 +1,34 @@ package one.mixin.android.ui.wallet -import one.mixin.android.ui.wallet.home.WalletHomeType +import com.google.gson.Gson +import one.mixin.android.db.property.PropertyHelper internal const val PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED = "pref_wallet_home_add_wallet_banner_closed" internal const val PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED = "pref_wallet_home_dynamic_banner_closed" internal const val PREF_WALLET_HOME_REFERRAL_CLOSED = "pref_wallet_home_referral_closed" -internal fun walletHomeDynamicBannerClosedKey( - walletType: WalletHomeType, - walletId: String, -) = "$PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED:${walletType.name}:$walletId" +private val walletHomePreferencesGson = Gson() + +internal fun walletHomeDynamicBannerClosedKey() = PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED + +internal suspend fun findWalletHomeDynamicBannerClosedIds(): Set { + val value = PropertyHelper.findValueByKey(walletHomeDynamicBannerClosedKey(), "") + if (value.isBlank()) return emptySet() + return runCatching { + walletHomePreferencesGson.fromJson(value, Array::class.java) + .orEmpty() + .filter(String::isNotBlank) + .toSet() + }.getOrDefault(emptySet()) +} + +internal suspend fun updateWalletHomeDynamicBannerClosedIds(closedIds: Set) { + if (closedIds.isEmpty()) { + PropertyHelper.deleteKeyValue(walletHomeDynamicBannerClosedKey()) + } else { + PropertyHelper.updateKeyValue( + walletHomeDynamicBannerClosedKey(), + walletHomePreferencesGson.toJson(closedIds.sorted()), + ) + } +} diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt index 780f9de4cf..785ae2fcec 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt @@ -52,7 +52,6 @@ import one.mixin.android.extension.openAsUrlOrWeb import one.mixin.android.extension.openUrl import one.mixin.android.extension.toast import one.mixin.android.extension.putBoolean -import one.mixin.android.extension.putStringSet import one.mixin.android.job.MixinJobManager import one.mixin.android.job.RefreshSnapshotsJob import one.mixin.android.job.RefreshTokensJob @@ -138,6 +137,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) private var topMovers: List = emptyList() private var pendingDisplays: List = emptyList() private var dynamicBanners: List = emptyList() + private var closedDynamicBannerIds: Set = emptySet() private val assetsAdapter by lazy { WalletAssetAdapter(false) } private val perpetualViewModel by viewModels() private var walletHomeDataState = WalletHomeDataState.EMPTY @@ -401,7 +401,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) fiatRate = fiatRate, ) val showAddWalletBanner = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED, false) - val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds()) + val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds) val showBanner = visibleDynamicBanners.isNotEmpty() || showAddWalletBanner val showReferral = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_REFERRAL_CLOSED, false) val cards = WalletHomeBuilder.build( @@ -478,23 +478,18 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) } } - private fun closedDynamicBannerIds(): Set = - defaultSharedPreferences.getStringSet(dynamicBannerClosedKey(), emptySet()).orEmpty() - - private fun syncClosedDynamicBannerIds(remoteBanners: List) { - val closedBannerIds = closedDynamicBannerIds() + private suspend fun syncClosedDynamicBannerIds(remoteBanners: List) { + val closedBannerIds = findWalletHomeDynamicBannerClosedIds() val syncedClosedBannerIds = closedBannerIds.syncedWalletHomeClosedBannerIds(remoteBanners) if (syncedClosedBannerIds != closedBannerIds) { - defaultSharedPreferences.putStringSet(dynamicBannerClosedKey(), syncedClosedBannerIds) + updateWalletHomeDynamicBannerClosedIds(syncedClosedBannerIds) } + closedDynamicBannerIds = syncedClosedBannerIds } private fun privacyWalletHomeCacheKey(): String = walletHomeCacheKey(WalletHomeType.PRIVACY, Session.getAccountId().orEmpty()) - private fun dynamicBannerClosedKey(): String = - walletHomeDynamicBannerClosedKey(WalletHomeType.PRIVACY, Session.getAccountId().orEmpty()) - private fun List.toWalletHomePositionSummary(): WalletHomePositionSummary? { if (isEmpty()) return null val totalMargin = positionMarginUsdTotal() @@ -549,11 +544,12 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) } override fun onDynamicBannerClosed(banner: WalletHomeBanner) { - defaultSharedPreferences.putStringSet( - dynamicBannerClosedKey(), - closedDynamicBannerIds().toMutableSet().apply { add(banner.key) }, - ) - renderHome() + lifecycleScope.launch { + val closedIds = closedDynamicBannerIds.toMutableSet().apply { add(banner.key) } + updateWalletHomeDynamicBannerClosedIds(closedIds) + closedDynamicBannerIds = closedIds + renderHome() + } } override fun onReferralClicked() { diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 6e16e7f858..4ea1034ad2 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -190,6 +190,8 @@ private fun DynamicBannerCard( onActionClick: (WalletHomeBanner, WalletHomeBannerAction) -> Unit, ) { val actions = banner.visibleActions + val hasDescription = actions.isEmpty() && banner.description?.isNotBlank() == true + val titleOnly = actions.isEmpty() && !hasDescription val iconShape = if (banner.hasButtonStyle) CircleShape else RoundedCornerShape(8.dp) val bottomPadding = if (banner.hasButtonStyle) 20.dp else 22.dp Row( @@ -223,9 +225,9 @@ private fun DynamicBannerCard( Text( text = banner.title.orEmpty(), color = MixinAppTheme.colors.textMinor, - fontSize = 16.sp, + fontSize = if (titleOnly) 14.sp else 16.sp, lineHeight = 20.sp, - fontWeight = FontWeight.W500, + fontWeight = if (titleOnly) FontWeight.W400 else FontWeight.W500, ) if (actions.isEmpty()) { banner.description?.takeIf { it.isNotBlank() }?.let { description -> @@ -249,7 +251,6 @@ private fun DynamicBannerCard( text = action.label.orEmpty(), onClick = { onActionClick(banner, action) }, modifier = Modifier.weight(1f, fill = false), - fontWeight = FontWeight.W400, ) } } diff --git a/app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt b/app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt index 7e012bf1bf..b6652cba18 100644 --- a/app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt +++ b/app/src/test/java/one/mixin/android/ui/wallet/WalletHomePreferencesTest.kt @@ -1,19 +1,14 @@ package one.mixin.android.ui.wallet -import one.mixin.android.ui.wallet.home.WalletHomeType import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals import org.junit.Test class WalletHomePreferencesTest { @Test - fun dynamicBannerClosedKeyIsScopedByWalletTypeAndWalletId() { - val classicWalletA = walletHomeDynamicBannerClosedKey(WalletHomeType.CLASSIC, "wallet-a") - val classicWalletB = walletHomeDynamicBannerClosedKey(WalletHomeType.CLASSIC, "wallet-b") - val privacyWalletA = walletHomeDynamicBannerClosedKey(WalletHomeType.PRIVACY, "wallet-a") - - assertEquals("pref_wallet_home_dynamic_banner_closed:CLASSIC:wallet-a", classicWalletA) - assertNotEquals(classicWalletA, classicWalletB) - assertNotEquals(classicWalletA, privacyWalletA) + fun dynamicBannerClosedKeyIsGlobal() { + assertEquals( + PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED, + walletHomeDynamicBannerClosedKey(), + ) } } From 78b4d1793599776ce7b90b32b486aae92760e3d9 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 15:48:22 +0800 Subject: [PATCH 03/12] fix(analytics): track wallet home banner by key --- .../java/one/mixin/android/util/analytics/AnalyticsTracker.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt index 50eb38397e..e5bde80861 100644 --- a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt +++ b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt @@ -153,8 +153,7 @@ object AnalyticsTracker { fun trackWalletHomeAdBanner(trackingKey: String?, source: String) { val key = trackingKey?.takeIf { it.isNotBlank() } ?: return - logEvent("wallet_home_ad_banner") { - putString("tracking_key", key) + logEvent(trackingKey) { putString("source", source) } } From 14614a561c5f933821a6ecaa193f29b66faaac5d Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 15:58:32 +0800 Subject: [PATCH 04/12] fix(wallet): handle dynamic banner title only style --- .../ui/wallet/home/components/BannerCards.kt | 15 +-- app/src/main/res/drawable/ic_deafult.xml | 94 ------------------- 2 files changed, 8 insertions(+), 101 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_deafult.xml diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 4ea1034ad2..d68cc48d87 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -190,8 +190,9 @@ private fun DynamicBannerCard( onActionClick: (WalletHomeBanner, WalletHomeBannerAction) -> Unit, ) { val actions = banner.visibleActions - val hasDescription = actions.isEmpty() && banner.description?.isNotBlank() == true - val titleOnly = actions.isEmpty() && !hasDescription + val description = banner.description?.takeIf { it.isNotBlank() } + val showDescription = actions.isEmpty() && description != null + val titleOnly = !showDescription val iconShape = if (banner.hasButtonStyle) CircleShape else RoundedCornerShape(8.dp) val bottomPadding = if (banner.hasButtonStyle) 20.dp else 22.dp Row( @@ -205,7 +206,7 @@ private fun DynamicBannerCard( if (iconUrl != null) { CoilImageCompat( model = iconUrl, - placeholder = R.drawable.ic_deafult, + placeholder = R.drawable.ic_avatar_place_holder, contentScale = ContentScale.Crop, modifier = Modifier .size(42.dp) @@ -213,7 +214,7 @@ private fun DynamicBannerCard( ) } else { Image( - painter = painterResource(id = R.drawable.ic_deafult), + painter = painterResource(id = R.drawable.ic_avatar_place_holder), contentDescription = null, modifier = Modifier .size(42.dp) @@ -229,8 +230,8 @@ private fun DynamicBannerCard( lineHeight = 20.sp, fontWeight = if (titleOnly) FontWeight.W400 else FontWeight.W500, ) - if (actions.isEmpty()) { - banner.description?.takeIf { it.isNotBlank() }?.let { description -> + if (showDescription) { + description.let { description -> Spacer(modifier = Modifier.height(6.dp)) Text( text = description, @@ -240,7 +241,7 @@ private fun DynamicBannerCard( fontWeight = FontWeight.W400, ) } - } else { + } else if (actions.isNotEmpty()) { Spacer(modifier = Modifier.height(12.dp)) Row( horizontalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/src/main/res/drawable/ic_deafult.xml b/app/src/main/res/drawable/ic_deafult.xml deleted file mode 100644 index 49120d3e2a..0000000000 --- a/app/src/main/res/drawable/ic_deafult.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 9e16da73f94a8363032697d417519d7c11cf052c Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 16:05:48 +0800 Subject: [PATCH 05/12] fix(wallet): animate banner height changes --- .../one/mixin/android/ui/wallet/home/components/BannerCards.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index d68cc48d87..5fa4d311dd 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -1,5 +1,7 @@ package one.mixin.android.ui.wallet.home.components +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -73,6 +75,7 @@ internal fun BannerPager( .fillMaxWidth() .padding(horizontal = 20.dp) .wrapContentHeight() + .animateContentSize(animationSpec = tween(durationMillis = 200)) .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor), ) { Column(modifier = Modifier.padding(horizontal = 20.dp)) { From d91cbd436338738d0931d4dc40b3f1b00564c446 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 16:29:03 +0800 Subject: [PATCH 06/12] fix(wallet): improve home ad banner clicks --- .../ui/wallet/home/components/BannerCards.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 5fa4d311dd..4ba6a9fc57 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -70,13 +70,28 @@ internal fun BannerPager( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { + val currentPage = pages.getOrNull(pagerState.currentPage) + val cardClickModifier = + when (currentPage) { + WalletHomeBannerPage.AddWallet -> Modifier.clickable { callbacks.onAddWalletClicked() } + is WalletHomeBannerPage.Dynamic -> { + if (currentPage.banner.actionUrl.isNullOrBlank()) { + Modifier + } else { + Modifier.clickable { callbacks.onDynamicBannerClicked(currentPage.banner) } + } + } + null -> Modifier + } Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp) .wrapContentHeight() .animateContentSize(animationSpec = tween(durationMillis = 200)) - .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor), + .clip(RoundedCornerShape(8.dp)) + .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor) + .then(cardClickModifier), ) { Column(modifier = Modifier.padding(horizontal = 20.dp)) { HorizontalPager( @@ -96,7 +111,6 @@ internal fun BannerPager( ) is WalletHomeBannerPage.Dynamic -> DynamicBannerCard( banner = bannerPage.banner, - onClick = callbacks::onDynamicBannerClicked, onActionClick = callbacks::onDynamicBannerActionClicked, ) } @@ -189,7 +203,6 @@ private fun BannerCard( @Composable private fun DynamicBannerCard( banner: WalletHomeBanner, - onClick: (WalletHomeBanner) -> Unit, onActionClick: (WalletHomeBanner, WalletHomeBannerAction) -> Unit, ) { val actions = banner.visibleActions @@ -201,8 +214,7 @@ private fun DynamicBannerCard( Row( modifier = Modifier .fillMaxWidth() - .padding(top = 22.dp, end = 22.dp, bottom = bottomPadding) - .clickable { onClick(banner) }, + .padding(top = 22.dp, end = 22.dp, bottom = bottomPadding), verticalAlignment = Alignment.Top, ) { val iconUrl = banner.iconUrl?.takeIf { it.isNotBlank() } @@ -235,7 +247,7 @@ private fun DynamicBannerCard( ) if (showDescription) { description.let { description -> - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( text = description, color = MixinAppTheme.colors.textAssist, From ae08a618adc897feac580a842213eb4f118a734c Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 20:15:29 +0800 Subject: [PATCH 07/12] fix(wallet): preserve banner icon shape --- .../android/ui/wallet/home/components/BannerCards.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 4ba6a9fc57..80f0ce18fb 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -209,7 +209,6 @@ private fun DynamicBannerCard( val description = banner.description?.takeIf { it.isNotBlank() } val showDescription = actions.isEmpty() && description != null val titleOnly = !showDescription - val iconShape = if (banner.hasButtonStyle) CircleShape else RoundedCornerShape(8.dp) val bottomPadding = if (banner.hasButtonStyle) 20.dp else 22.dp Row( modifier = Modifier @@ -223,17 +222,13 @@ private fun DynamicBannerCard( model = iconUrl, placeholder = R.drawable.ic_avatar_place_holder, contentScale = ContentScale.Crop, - modifier = Modifier - .size(42.dp) - .clip(iconShape), + modifier = Modifier.size(42.dp), ) } else { Image( painter = painterResource(id = R.drawable.ic_avatar_place_holder), contentDescription = null, - modifier = Modifier - .size(42.dp) - .clip(iconShape), + modifier = Modifier.size(42.dp), ) } Spacer(modifier = Modifier.width(14.dp)) From 35133d396d823ba4a127ad1ce9922c63f2837642 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 21:38:51 +0800 Subject: [PATCH 08/12] fix(wallet): smooth banner rotation timing --- .../ui/wallet/home/components/BannerCards.kt | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 80f0ce18fb..8943372e76 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -26,7 +26,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -42,6 +46,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay import one.mixin.android.R import one.mixin.android.api.response.WalletHomeBanner import one.mixin.android.api.response.WalletHomeBannerAction @@ -51,20 +56,51 @@ import one.mixin.android.ui.wallet.alert.components.cardBackground import one.mixin.android.ui.wallet.home.WalletHomeCallbacks import one.mixin.android.ui.wallet.home.WalletHomeState +private const val LOCAL_BANNER_SHOW_DELAY_MILLIS = 2_000L +private const val BANNER_AUTO_SWITCH_DELAY_MILLIS = 3_000L + @OptIn(ExperimentalFoundationApi::class) @Composable internal fun BannerPager( state: WalletHomeState, callbacks: WalletHomeCallbacks, ) { - val pages = remember(state.showAddWalletBanner, state.dynamicBanners) { + var showLocalBanner by remember(state.showAddWalletBanner, state.dynamicBanners) { + mutableStateOf(!state.showAddWalletBanner || state.dynamicBanners.isNotEmpty()) + } + var showLocalBannerFirst by remember(state.showAddWalletBanner) { + mutableStateOf(false) + } + LaunchedEffect(state.showAddWalletBanner, state.dynamicBanners) { + if (!state.showAddWalletBanner) { + showLocalBanner = true + showLocalBannerFirst = false + } else if (state.dynamicBanners.isNotEmpty()) { + showLocalBanner = true + } else { + delay(LOCAL_BANNER_SHOW_DELAY_MILLIS) + showLocalBanner = true + showLocalBannerFirst = true + } + } + val showAddWalletBanner = state.showAddWalletBanner && showLocalBanner + val pages = remember(showAddWalletBanner, showLocalBannerFirst, state.dynamicBanners) { buildList { + if (showAddWalletBanner && showLocalBannerFirst) add(WalletHomeBannerPage.AddWallet) addAll(state.dynamicBanners.map(WalletHomeBannerPage::Dynamic)) - if (state.showAddWalletBanner) add(WalletHomeBannerPage.AddWallet) + if (showAddWalletBanner && !showLocalBannerFirst) add(WalletHomeBannerPage.AddWallet) } } if (pages.isEmpty()) return val pagerState = rememberPagerState(initialPage = 0) { pages.size } + LaunchedEffect(pages.size) { + if (pages.size <= 1) return@LaunchedEffect + while (true) { + delay(BANNER_AUTO_SWITCH_DELAY_MILLIS) + val nextPage = (pagerState.currentPage + 1) % pages.size + pagerState.animateScrollToPage(nextPage) + } + } Column( modifier = Modifier.fillMaxWidth(), From 18b5fc7c5ec18e165ae3a37f62ba87a6075a0dc0 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 22:03:47 +0800 Subject: [PATCH 09/12] fix(wallet): pause banner auto scroll while dragging --- .../ui/wallet/home/components/BannerCards.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 8943372e76..07d407e407 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -47,6 +48,8 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull import one.mixin.android.R import one.mixin.android.api.response.WalletHomeBanner import one.mixin.android.api.response.WalletHomeBannerAction @@ -96,9 +99,17 @@ internal fun BannerPager( LaunchedEffect(pages.size) { if (pages.size <= 1) return@LaunchedEffect while (true) { - delay(BANNER_AUTO_SWITCH_DELAY_MILLIS) - val nextPage = (pagerState.currentPage + 1) % pages.size - pagerState.animateScrollToPage(nextPage) + if (pagerState.isScrollInProgress) { + snapshotFlow { pagerState.isScrollInProgress }.first { !it } + } + val interrupted = withTimeoutOrNull(BANNER_AUTO_SWITCH_DELAY_MILLIS) { + snapshotFlow { pagerState.isScrollInProgress }.first { it } + true + } ?: false + if (!interrupted && !pagerState.isScrollInProgress) { + val nextPage = (pagerState.currentPage + 1) % pages.size + pagerState.animateScrollToPage(nextPage) + } } } From 3800c551c02b8adc2a2e8d36035c89baffbaca32 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 25 Jun 2026 13:12:37 +0800 Subject: [PATCH 10/12] fix(wallet): wait for banner request completion --- .../ui/wallet/WalletHomeClassicFragment.kt | 18 +++++++++-- .../ui/wallet/WalletHomePrivacyFragment.kt | 17 +++++++++-- .../android/ui/wallet/home/WalletHomeState.kt | 1 + .../ui/wallet/home/components/BannerCards.kt | 30 ++----------------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt index efba4ff8f3..c064426413 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomeClassicFragment.kt @@ -141,6 +141,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) private var pendingTransactionCount: Int = 0 private var watchAddresses: List = emptyList() private var dynamicBanners: List = emptyList() + private var isDynamicBannerLoaded = false private var closedDynamicBannerIds: Set = emptySet() private val assetsAdapter by lazy { WalletWeb3TokenAdapter(false) } @@ -165,6 +166,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) field = value walletHomeDataState = WalletHomeDataState.EMPTY dynamicBanners = emptyList() + isDynamicBannerLoaded = false _walletId.value = value loadWalletHomeCache() } @@ -499,7 +501,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) ) val showAddWalletBanner = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED, false) val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds) - val showBanner = visibleDynamicBanners.isNotEmpty() || showAddWalletBanner + val showBanner = isDynamicBannerLoaded && (visibleDynamicBanners.isNotEmpty() || showAddWalletBanner) val showReferral = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_REFERRAL_CLOSED, false) val currentImportKeyAction = importKeyAction val pendingCount = walletHomePendingTransactionCount(pendingRawTransactionCount, pendingTransactionCount) @@ -533,6 +535,7 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) watchIndicator = if (isWatchWallet) walletHomeWatchIndicator(watchAddresses) else null, importKeyAction = currentImportKeyAction, showAddWalletBanner = showAddWalletBanner, + isDynamicBannerLoaded = isDynamicBannerLoaded, dynamicBanners = visibleDynamicBanners, showReferralBanner = showReferral, showImportSafetyFooter = !isLoading, @@ -609,9 +612,18 @@ class WalletHomeClassicFragment : BaseFragment(R.layout.fragment_privacy_wallet) fun refreshWalletHomeBanners() { if (!isAdded || walletId.isEmpty()) return lifecycleScope.launch { - val remoteBanners = web3ViewModel.walletHomeBanners(walletHomeBannerChains()) - syncClosedDynamicBannerIds(remoteBanners) + val remoteBanners = runCatching { + web3ViewModel.walletHomeBanners(walletHomeBannerChains()) + }.onFailure { + Timber.w(it, "Fetch wallet home banners failed") + }.getOrDefault(emptyList()) + runCatching { + syncClosedDynamicBannerIds(remoteBanners) + }.onFailure { + Timber.w(it, "Sync wallet home banner closed ids failed") + } dynamicBanners = remoteBanners + isDynamicBannerLoaded = true renderHome() } } diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt index 785ae2fcec..0d507e46ed 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt @@ -137,6 +137,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) private var topMovers: List = emptyList() private var pendingDisplays: List = emptyList() private var dynamicBanners: List = emptyList() + private var isDynamicBannerLoaded = false private var closedDynamicBannerIds: Set = emptySet() private val assetsAdapter by lazy { WalletAssetAdapter(false) } private val perpetualViewModel by viewModels() @@ -402,7 +403,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) ) val showAddWalletBanner = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_ADD_WALLET_BANNER_CLOSED, false) val visibleDynamicBanners = dynamicBanners.visibleWalletHomeBanners(closedDynamicBannerIds) - val showBanner = visibleDynamicBanners.isNotEmpty() || showAddWalletBanner + val showBanner = isDynamicBannerLoaded && (visibleDynamicBanners.isNotEmpty() || showAddWalletBanner) val showReferral = !defaultSharedPreferences.getBoolean(PREF_WALLET_HOME_REFERRAL_CLOSED, false) val cards = WalletHomeBuilder.build( walletType = WalletHomeType.PRIVACY, @@ -435,6 +436,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) pendingIndicator = pendingDisplays.toWalletHomePendingIndicator(), quoteColorReversed = defaultSharedPreferences.getBoolean(Constants.Account.PREF_QUOTE_COLOR, false), showAddWalletBanner = showAddWalletBanner, + isDynamicBannerLoaded = isDynamicBannerLoaded, dynamicBanners = visibleDynamicBanners, showReferralBanner = showReferral, showBuyBadge = defaultSharedPreferences.getBoolean(PREF_HAS_USED_BUY, true), @@ -471,9 +473,18 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet) fun refreshWalletHomeBanners() { lifecycleScope.launch { - val remoteBanners = walletViewModel.walletHomeBanners() - syncClosedDynamicBannerIds(remoteBanners) + val remoteBanners = runCatching { + walletViewModel.walletHomeBanners() + }.onFailure { + Timber.w(it, "Fetch wallet home banners failed") + }.getOrDefault(emptyList()) + runCatching { + syncClosedDynamicBannerIds(remoteBanners) + }.onFailure { + Timber.w(it, "Sync wallet home banner closed ids failed") + } dynamicBanners = remoteBanners + isDynamicBannerLoaded = true renderHome() } } diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt index cce87436ff..5889789d76 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt @@ -40,6 +40,7 @@ data class WalletHomeState( val hideActions: Boolean = false, val quoteColorReversed: Boolean = false, val showAddWalletBanner: Boolean = false, + val isDynamicBannerLoaded: Boolean = false, val dynamicBanners: List = emptyList(), val showReferralBanner: Boolean = false, val showBuyBadge: Boolean = false, diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index 07d407e407..fa95f34cca 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -27,10 +27,7 @@ import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,7 +44,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull import one.mixin.android.R @@ -59,7 +55,6 @@ import one.mixin.android.ui.wallet.alert.components.cardBackground import one.mixin.android.ui.wallet.home.WalletHomeCallbacks import one.mixin.android.ui.wallet.home.WalletHomeState -private const val LOCAL_BANNER_SHOW_DELAY_MILLIS = 2_000L private const val BANNER_AUTO_SWITCH_DELAY_MILLIS = 3_000L @OptIn(ExperimentalFoundationApi::class) @@ -68,30 +63,11 @@ internal fun BannerPager( state: WalletHomeState, callbacks: WalletHomeCallbacks, ) { - var showLocalBanner by remember(state.showAddWalletBanner, state.dynamicBanners) { - mutableStateOf(!state.showAddWalletBanner || state.dynamicBanners.isNotEmpty()) - } - var showLocalBannerFirst by remember(state.showAddWalletBanner) { - mutableStateOf(false) - } - LaunchedEffect(state.showAddWalletBanner, state.dynamicBanners) { - if (!state.showAddWalletBanner) { - showLocalBanner = true - showLocalBannerFirst = false - } else if (state.dynamicBanners.isNotEmpty()) { - showLocalBanner = true - } else { - delay(LOCAL_BANNER_SHOW_DELAY_MILLIS) - showLocalBanner = true - showLocalBannerFirst = true - } - } - val showAddWalletBanner = state.showAddWalletBanner && showLocalBanner - val pages = remember(showAddWalletBanner, showLocalBannerFirst, state.dynamicBanners) { + val showAddWalletBanner = state.showAddWalletBanner && state.isDynamicBannerLoaded + val pages = remember(showAddWalletBanner, state.dynamicBanners) { buildList { - if (showAddWalletBanner && showLocalBannerFirst) add(WalletHomeBannerPage.AddWallet) addAll(state.dynamicBanners.map(WalletHomeBannerPage::Dynamic)) - if (showAddWalletBanner && !showLocalBannerFirst) add(WalletHomeBannerPage.AddWallet) + if (showAddWalletBanner) add(WalletHomeBannerPage.AddWallet) } } if (pages.isEmpty()) return From d0356d99c5f891ec13b73b3834e4330dcbb91499 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 25 Jun 2026 17:17:15 +0800 Subject: [PATCH 11/12] fix(wallet): refine banner card layout --- .../ui/wallet/home/components/BannerCards.kt | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt index fa95f34cca..74dc506744 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/BannerCards.kt @@ -1,7 +1,5 @@ package one.mixin.android.ui.wallet.home.components -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -27,13 +25,18 @@ import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -72,6 +75,13 @@ internal fun BannerPager( } if (pages.isEmpty()) return val pagerState = rememberPagerState(initialPage = 0) { pages.size } + val density = LocalDensity.current + var bannerHeightPx by remember(pages) { mutableIntStateOf(0) } + val bannerHeightModifier = if (bannerHeightPx > 0) { + Modifier.height(with(density) { bannerHeightPx.toDp() }) + } else { + Modifier.wrapContentHeight() + } LaunchedEffect(pages.size) { if (pages.size <= 1) return@LaunchedEffect while (true) { @@ -110,8 +120,7 @@ internal fun BannerPager( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp) - .wrapContentHeight() - .animateContentSize(animationSpec = tween(durationMillis = 200)) + .then(bannerHeightModifier) .clip(RoundedCornerShape(8.dp)) .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor) .then(cardClickModifier), @@ -122,20 +131,30 @@ internal fun BannerPager( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - verticalAlignment = Alignment.CenterVertically, + beyondViewportPageCount = pages.size, + verticalAlignment = Alignment.Top, ) { page -> - when (val bannerPage = pages[page]) { - WalletHomeBannerPage.AddWallet -> BannerCard( - iconRes = R.drawable.ic_wallet_home_add, - titleRes = R.string.wallet_home_add_wallet_banner_title, - descriptionRes = null, - ctaRes = R.string.add_wallet, - onClick = callbacks::onAddWalletClicked, - ) - is WalletHomeBannerPage.Dynamic -> DynamicBannerCard( - banner = bannerPage.banner, - onActionClick = callbacks::onDynamicBannerActionClicked, - ) + Box( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { size -> + bannerHeightPx = maxOf(bannerHeightPx, size.height) + }, + contentAlignment = Alignment.TopStart, + ) { + when (val bannerPage = pages[page]) { + WalletHomeBannerPage.AddWallet -> BannerCard( + iconRes = R.drawable.ic_wallet_home_add, + titleRes = R.string.wallet_home_add_wallet_banner_title, + descriptionRes = null, + ctaRes = R.string.add_wallet, + onClick = callbacks::onAddWalletClicked, + ) + is WalletHomeBannerPage.Dynamic -> DynamicBannerCard( + banner = bannerPage.banner, + onActionClick = callbacks::onDynamicBannerActionClicked, + ) + } } } } @@ -192,7 +211,7 @@ private fun BannerCard( Row( modifier = Modifier .fillMaxWidth() - .padding(top = 20.dp, end = 22.dp, bottom = 20.dp), + .padding(top = 16.dp, end = 22.dp, bottom = 16.dp), verticalAlignment = Alignment.Top, ) { Image( @@ -204,10 +223,13 @@ private fun BannerCard( Column(modifier = Modifier.weight(1f)) { Text( text = stringResource(titleRes), + modifier = Modifier.fillMaxWidth(), color = MixinAppTheme.colors.textMinor, fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.W400, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) if (descriptionRes != null) { Spacer(modifier = Modifier.height(4.dp)) @@ -215,9 +237,11 @@ private fun BannerCard( text = stringResource(descriptionRes), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(8.dp)) BannerAction(text = stringResource(ctaRes), onClick = onClick) } } @@ -232,11 +256,10 @@ private fun DynamicBannerCard( val description = banner.description?.takeIf { it.isNotBlank() } val showDescription = actions.isEmpty() && description != null val titleOnly = !showDescription - val bottomPadding = if (banner.hasButtonStyle) 20.dp else 22.dp Row( modifier = Modifier .fillMaxWidth() - .padding(top = 22.dp, end = 22.dp, bottom = bottomPadding), + .padding(top = 16.dp, end = 22.dp, bottom = 16.dp), verticalAlignment = Alignment.Top, ) { val iconUrl = banner.iconUrl?.takeIf { it.isNotBlank() } @@ -258,10 +281,13 @@ private fun DynamicBannerCard( Column(modifier = Modifier.weight(1f)) { Text( text = banner.title.orEmpty(), + modifier = Modifier.fillMaxWidth(), color = MixinAppTheme.colors.textMinor, fontSize = if (titleOnly) 14.sp else 16.sp, lineHeight = 20.sp, fontWeight = if (titleOnly) FontWeight.W400 else FontWeight.W500, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) if (showDescription) { description.let { description -> @@ -272,10 +298,12 @@ private fun DynamicBannerCard( fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.W400, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } else if (actions.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(8.dp)) Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, From 192fae36224952db87725eb02fb02e084eb35979 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Fri, 26 Jun 2026 12:33:24 +0800 Subject: [PATCH 12/12] fix(wallet): address ad banner review feedback --- app/src/main/java/one/mixin/android/di/AppModule.kt | 2 +- .../java/one/mixin/android/util/analytics/AnalyticsTracker.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/one/mixin/android/di/AppModule.kt b/app/src/main/java/one/mixin/android/di/AppModule.kt index 79661bc904..a5fa6f96b2 100644 --- a/app/src/main/java/one/mixin/android/di/AppModule.kt +++ b/app/src/main/java/one/mixin/android/di/AppModule.kt @@ -531,7 +531,7 @@ object AppModule { val sourceRequest = chain.request() val b = sourceRequest.newBuilder() b.addHeader("User-Agent", API_UA) - .addHeader("Accept-Language", Locale.getDefault().language + "-" + Locale.getDefault().country) + .addHeader("Accept-Language", Locale.getDefault().toLanguageTag()) .addHeader("Mixin-Device-Id", getStringDeviceId(resolver)) .addHeader(xRequestId, UUID.randomUUID().toString()) val botPublicKey = appContext.defaultSharedPreferences.getString(PREF_ROUTE_BOT_PK, null) diff --git a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt index e5bde80861..b06c67e1ed 100644 --- a/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt +++ b/app/src/main/java/one/mixin/android/util/analytics/AnalyticsTracker.kt @@ -153,7 +153,7 @@ object AnalyticsTracker { fun trackWalletHomeAdBanner(trackingKey: String?, source: String) { val key = trackingKey?.takeIf { it.isNotBlank() } ?: return - logEvent(trackingKey) { + logEvent(key) { putString("source", source) } }