Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/src/main/java/one/mixin/android/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WalletHomeBannerAction>? = 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<String>? = 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<WalletHomeBannerAction>
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<String>.syncedWalletHomeClosedBannerIds(remoteBanners: List<WalletHomeBanner>): Set<String> {
val remoteKeys = remoteBanners.mapNotNull { it.key.takeIf(String::isNotBlank) }.toSet()
return filter { remoteKeys.contains(it) }.toSet()
}

fun List<WalletHomeBanner>.visibleWalletHomeBanners(closedBannerIds: Set<String>): List<WalletHomeBanner> =
filter { banner ->
banner.key.isNotBlank() &&
banner.isActive &&
banner.hasVisualContent &&
(!banner.actionUrl.isNullOrBlank() || banner.visibleActions.isNotEmpty()) &&
!closedBannerIds.contains(banner.key)
}.sortedByDescending { it.priority }
Original file line number Diff line number Diff line change
@@ -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<ReferralResponse>

@GET("app-banners")
suspend fun walletHomeBanners(
@Query("chain") chains: List<String>? = null,
): MixinResponse<List<WalletHomeBanner>>
}
2 changes: 1 addition & 1 deletion app/src/main/java/one/mixin/android/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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().toLanguageTag())
.addHeader("Mixin-Device-Id", getStringDeviceId(resolver))
Comment on lines 532 to 535
.addHeader(xRequestId, UUID.randomUUID().toString())
val botPublicKey = appContext.defaultSharedPreferences.getString(PREF_ROUTE_BOT_PK, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +89,33 @@ class ReferralRepository
requestSession = { userRepository.fetchSessionsSuspend(it) },
)
}

suspend fun fetchWalletHomeBanners(chains: List<String> = emptyList()): List<WalletHomeBanner> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -97,6 +99,8 @@ class Web3ViewModel @Inject constructor(

suspend fun findOrSyncApp(appId: String) = userRepository.findOrSyncApp(appId)

suspend fun walletHomeBanners(chains: List<String>) = referralRepository.fetchWalletHomeBanners(chains)

suspend fun findMarketItemByAssetId(assetId: String) = tokenRepository.findMarketItemByAssetId(assetId)

fun web3TokensExcludeHidden(walletId: String) = web3Repository.web3TokensExcludeHidden(walletId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +40,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
Expand Down Expand Up @@ -143,10 +145,17 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) {
}

resetTpslGuide.setOnClickListener {
resetDebugSharedPreferences()
resetHiddenDebugSharedPreferences()
toast(R.string.Reset_TpSl_Guide)
}

resetWalletHomeBanners.setOnClickListener {
lifecycleScope.launch {
resetWalletHomeBannerLocalState()
toast(R.string.Reset_Wallet_Home_Banners)
}
}

deleteWeb3Transactions.setOnClickListener {
context?.let { ctx ->
alertDialogBuilder()
Expand Down Expand Up @@ -240,36 +249,22 @@ 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<String> =
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 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() {
val dialog =
Expand Down Expand Up @@ -317,3 +312,35 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) {
}
}
}

private fun hiddenDebugSharedPreferenceKeys(accountId: String?): List<String> =
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<String>): Set<String> =
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)
add(Constants.Account.PREF_HAS_USED_ADD_WALLET)
existingKeys
.filter { it.startsWith("$PREF_WALLET_HOME_DYNAMIC_BANNER_CLOSED:") }
.forEach(::add)
}
16 changes: 16 additions & 0 deletions app/src/main/java/one/mixin/android/ui/wallet/WalletFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,22 @@ class WalletFragment : BaseFragment(R.layout.fragment_wallet) {
} else {
update()
}
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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Loading