diff --git a/app/src/main/java/one/mixin/android/ui/wallet/AllWeb3TransactionsFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/AllWeb3TransactionsFragment.kt index a26d56fca4..4625169f0a 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/AllWeb3TransactionsFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/AllWeb3TransactionsFragment.kt @@ -46,6 +46,7 @@ import one.mixin.android.ui.wallet.Web3FilterParams.Companion.FILTER_MASK import one.mixin.android.ui.wallet.adapter.Web3TransactionPagedAdapter import one.mixin.android.util.viewBinding import one.mixin.android.web3.details.Web3TransactionFragment +import one.mixin.android.web3.details.navigateToWeb3Transaction import one.mixin.android.web3.details.openWeb3AssetTransactions import one.mixin.android.web3.details.tokenNavigationTarget import one.mixin.android.widget.BottomSheet @@ -67,7 +68,7 @@ class AllWeb3TransactionsFragment : BaseTransactionsFragment(R.layout.fragment_a lifecycleScope.launch { val token = web3ViewModel.web3TokenItemById(filterParams.walletId,transaction.getMainAssetId()) ?: return@launch val wallet = web3ViewModel.findWalletById(filterParams.walletId) ?: return@launch - this@AllWeb3TransactionsFragment.view?.findNavController()?.navigate( + this@AllWeb3TransactionsFragment.view?.findNavController()?.navigateToWeb3Transaction( R.id.action_all_web3_transactions_fragment_to_web3_transaction_fragment, Bundle().apply { putParcelable("args_transaction", transaction) diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt index 0d2efdfb5d..fdd03fafbf 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeCache.kt @@ -121,6 +121,14 @@ internal fun String?.parseWalletHomeCache(): WalletHomeCache? = } } } + root.get("importKeyAction")?.let { action -> + val kind = if (action.isJsonObject) action.asJsonObject.get("kind") else null + if (kind == null || !kind.isJsonPrimitive || !kind.asJsonPrimitive.isString || + WalletHomeImportKeyKind.entries.none { it.name == kind.asString } + ) { + root.remove("importKeyAction") + } + } GsonHelper.customGson.fromJson(root, WalletHomeCache::class.java) }.getOrNull() 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 e24f5b7082..84b2b59e86 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 @@ -1,5 +1,6 @@ package one.mixin.android.ui.wallet.home +import com.google.gson.annotations.SerializedName import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.api.response.perps.PerpsMarket import one.mixin.android.R @@ -129,15 +130,21 @@ enum class WalletHomeWatchKind { } data class WalletHomeImportKeyAction( + @SerializedName("kind") val kind: WalletHomeImportKeyKind, +) { + val buttonTextRes: Int get() = kind.buttonTextRes + val descriptionRes: Int get() = kind.descriptionRes + val learnMoreUrlRes: Int get() = kind.learnMoreUrlRes +} + +enum class WalletHomeImportKeyKind( val buttonTextRes: Int, val descriptionRes: Int, val learnMoreUrlRes: Int, -) - -enum class WalletHomeImportKeyKind { - MNEMONIC_PHRASE, - PRIVATE_KEY, +) { + MNEMONIC_PHRASE(R.string.import_mnemonic_phrase, R.string.Import_Mnemonic_Phrase_Desc, R.string.import_mnemonic_phrase_url), + PRIVATE_KEY(R.string.import_private_key, R.string.Import_Private_Key_Desc, R.string.import_private_key_url), } fun List.toWalletHomePendingIndicator(): WalletHomePendingIndicator? { @@ -208,18 +215,8 @@ fun walletHomeImportKeyAction( ): WalletHomeImportKeyAction? { if (hasLocalPrivateKey) return null return when (category) { - WalletCategory.IMPORTED_MNEMONIC.value -> WalletHomeImportKeyAction( - kind = WalletHomeImportKeyKind.MNEMONIC_PHRASE, - buttonTextRes = R.string.import_mnemonic_phrase, - descriptionRes = R.string.Import_Mnemonic_Phrase_Desc, - learnMoreUrlRes = R.string.import_mnemonic_phrase_url, - ) - WalletCategory.IMPORTED_PRIVATE_KEY.value -> WalletHomeImportKeyAction( - kind = WalletHomeImportKeyKind.PRIVATE_KEY, - buttonTextRes = R.string.import_private_key, - descriptionRes = R.string.Import_Private_Key_Desc, - learnMoreUrlRes = R.string.import_private_key_url, - ) + WalletCategory.IMPORTED_MNEMONIC.value -> WalletHomeImportKeyAction(WalletHomeImportKeyKind.MNEMONIC_PHRASE) + WalletCategory.IMPORTED_PRIVATE_KEY.value -> WalletHomeImportKeyAction(WalletHomeImportKeyKind.PRIVATE_KEY) else -> null } } diff --git a/app/src/main/java/one/mixin/android/web3/details/Web3TokenNavigation.kt b/app/src/main/java/one/mixin/android/web3/details/Web3TokenNavigation.kt index 50f92d3512..c4ebf42794 100644 --- a/app/src/main/java/one/mixin/android/web3/details/Web3TokenNavigation.kt +++ b/app/src/main/java/one/mixin/android/web3/details/Web3TokenNavigation.kt @@ -1,8 +1,11 @@ package one.mixin.android.web3.details import android.os.Bundle +import androidx.annotation.IdRes import androidx.fragment.app.Fragment import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController import one.mixin.android.R import one.mixin.android.db.web3.vo.Web3TokenItem @@ -62,16 +65,9 @@ private fun Fragment.navigateToWeb3Asset( address: String?, ): Boolean { val nav = runCatching { findNavController() }.getOrNull() ?: return false - if (nav.currentDestination?.id == R.id.web3_transactions_fragment && - nav.currentBackStackEntry?.tokenArgument()?.assetId == token.assetId - ) { - return true - } - val previous = nav.previousBackStackEntry - if (previous?.destination?.id == R.id.web3_transactions_fragment && - previous.tokenArgument()?.assetId == token.assetId - ) { - activity?.onBackPressedDispatcher?.onBackPressed() + val existingToken = nav.findBackStackEntry(R.id.web3_transactions_fragment)?.tokenArgument() + if (existingToken?.assetId == token.assetId && existingToken.walletId == token.walletId) { + nav.popBackStack(R.id.web3_transactions_fragment, false) return true } val action = when (nav.currentDestination?.id) { @@ -84,8 +80,39 @@ private fun Fragment.navigateToWeb3Asset( putParcelable(Web3TransactionsFragment.ARGS_TOKEN, token) if (!address.isNullOrEmpty()) putString(Web3TransactionsFragment.ARGS_ADDRESS, address) } - return runCatching { nav.navigate(action, args) }.isSuccess + return runCatching { nav.navigateReplacingExisting(action, args, R.id.web3_transactions_fragment) }.isSuccess +} + +internal fun NavController.navigateToWeb3Transaction( + @IdRes resId: Int, + args: Bundle, +) { + navigateReplacingExisting(resId, args, R.id.web3_transaction_fragment) +} + +// Keep at most one entry of destinationId in the back stack: pop the existing one (and everything above it) before navigating. +private fun NavController.navigateReplacingExisting( + @IdRes resId: Int, + args: Bundle, + @IdRes destinationId: Int, +) { + if (findBackStackEntry(destinationId) == null) { + navigate(resId, args) + return + } + val actionOptions = currentDestination?.getAction(resId)?.navOptions + val options = NavOptions.Builder() + .setEnterAnim(actionOptions?.enterAnim ?: -1) + .setExitAnim(actionOptions?.exitAnim ?: -1) + .setPopEnterAnim(actionOptions?.popEnterAnim ?: -1) + .setPopExitAnim(actionOptions?.popExitAnim ?: -1) + .setPopUpTo(destinationId, true) + .build() + navigate(resId, args, options) } +private fun NavController.findBackStackEntry(@IdRes destinationId: Int): NavBackStackEntry? = + runCatching { getBackStackEntry(destinationId) }.getOrNull() + private fun NavBackStackEntry.tokenArgument(): Web3TokenItem? = arguments?.getParcelableCompat(Web3TransactionsFragment.ARGS_TOKEN, Web3TokenItem::class.java) diff --git a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt index d1e5ff3210..5acceea2a1 100644 --- a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt +++ b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionsFragment.kt @@ -613,7 +613,7 @@ class Web3TransactionsFragment : BaseFragment(R.layout.fragment_web3_transaction putParcelable(ARGS_TOKEN, token) putParcelable(Web3TransactionFragment.ARGS_WALLET, wallet?.toWeb3Wallet()) } - findNavController().navigate( + findNavController().navigateToWeb3Transaction( R.id.action_web3_transactions_to_web3_transaction, bundle ) diff --git a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt index 80c4430a1c..3b7bb85ac4 100644 --- a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt +++ b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeCashAccountTest.kt @@ -1,6 +1,7 @@ package one.mixin.android.ui.wallet.home import com.google.gson.Gson +import one.mixin.android.R import one.mixin.android.api.response.CashAccount import one.mixin.android.api.response.WalletHomeBanner import one.mixin.android.util.GsonHelper @@ -182,6 +183,89 @@ class WalletHomeCashAccountTest { assertNull(cache?.toState()?.earnAccounts?.single()?.maxApyText) } + @Test + fun walletHomeCacheIgnoresStaleImportKeyResourceIds() { + val json = """ + { + "walletType":"CLASSIC", + "fiatTotal":"0.00", + "btcTotal":"0.00", + "fiatSymbol":"USD", + "totalTokenCount":0, + "totalTransactionCount":0, + "importKeyAction":{"kind":"MNEMONIC_PHRASE","buttonTextRes":1,"descriptionRes":2,"learnMoreUrlRes":3} + } + """.trimIndent() + + val action = json.parseWalletHomeCache()?.toState()?.importKeyAction + + assertEquals(R.string.import_mnemonic_phrase, action?.buttonTextRes) + assertEquals(R.string.Import_Mnemonic_Phrase_Desc, action?.descriptionRes) + assertEquals(R.string.import_mnemonic_phrase_url, action?.learnMoreUrlRes) + assertEquals("""{"kind":"MNEMONIC_PHRASE"}""", GsonHelper.customGson.toJson(action)) + } + + @Test + fun walletHomeCacheDropsInvalidImportKeyActions() { + val actions = listOf( + """{"a":"MNEMONIC_PHRASE","b":1,"c":2,"d":3}""", + """{}""", + """{"kind":null}""", + """{"kind":"UNKNOWN"}""", + """{"kind":1}""", + """{"kind":true}""", + """{"kind":[]}""", + """{"kind":{}}""", + """null""", + """[]""", + """"MNEMONIC_PHRASE"""", + ) + + for (action in actions) { + val json = """ + { + "walletType":"CLASSIC", + "fiatTotal":"12.34", + "btcTotal":"0.00", + "fiatSymbol":"USD", + "totalTokenCount":0, + "totalTransactionCount":0, + "importKeyAction":$action + } + """.trimIndent() + + val cache = json.parseWalletHomeCache() + + assertNotNull(action, cache) + assertNull(action, cache?.importKeyAction) + val state = cache?.toState() + assertNull(action, state?.importKeyAction) + assertEquals(action, "12.34", state?.fiatTotal) + } + } + + @Test + fun walletHomeCacheRestoresPrivateKeyAction() { + val cache = WalletHomeCache( + walletType = WalletHomeType.CLASSIC, + fiatTotal = "0.00", + btcTotal = "0.00", + fiatSymbol = "USD", + totalTokenCount = 0, + totalTransactionCount = 0, + importKeyAction = WalletHomeImportKeyAction(WalletHomeImportKeyKind.PRIVATE_KEY), + ) + val json = GsonHelper.customGson.toJson(cache) + + val action = json.parseWalletHomeCache()?.toState()?.importKeyAction + + assertEquals(WalletHomeImportKeyKind.PRIVATE_KEY, action?.kind) + assertEquals(R.string.import_private_key, action?.buttonTextRes) + assertEquals(R.string.Import_Private_Key_Desc, action?.descriptionRes) + assertEquals(R.string.import_private_key_url, action?.learnMoreUrlRes) + assertEquals("""{"kind":"PRIVATE_KEY"}""", GsonHelper.customGson.toJson(action)) + } + @Test fun walletHomeStateShowsDynamicBannerWhenStillLoading() { val banner = WalletHomeBanner(