feat: add copy button to seed phrase backup screen

Allow users to copy their mnemonic phrase to clipboard from the manual
backup screen for easier migration to Welati Telegram mini app.
This commit is contained in:
2026-02-20 05:15:47 +03:00
parent 03075104b4
commit eb2c6cda55
10 changed files with 76 additions and 12 deletions
@@ -61,4 +61,8 @@ class ManualBackupAdvancedSecretsFragment :
override fun onTapToRevealClicked(item: ManualBackupSecretsVisibilityRvItem) { override fun onTapToRevealClicked(item: ManualBackupSecretsVisibilityRvItem) {
viewModel.onTapToRevealClicked(item) viewModel.onTapToRevealClicked(item)
} }
override fun onMnemonicCopyClicked(mnemonicString: String) {
viewModel.copyMnemonic(mnemonicString)
}
} }
@@ -1,6 +1,7 @@
package io.novafoundation.nova.feature_account_impl.presentation.manualBackup.secrets.advanced package io.novafoundation.nova.feature_account_impl.presentation.manualBackup.secrets.advanced
import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.resources.ClipboardManager
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.flowOf
import io.novafoundation.nova.feature_account_impl.R import io.novafoundation.nova.feature_account_impl.R
@@ -16,7 +17,8 @@ class ManualBackupAdvancedSecretsViewModel(
private val resourceManager: ResourceManager, private val resourceManager: ResourceManager,
private val router: AccountRouter, private val router: AccountRouter,
private val payload: ManualBackupCommonPayload, private val payload: ManualBackupCommonPayload,
private val secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory private val secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory,
private val clipboardManager: ClipboardManager
) : BaseViewModel() { ) : BaseViewModel() {
val exportList = flowOf { buildSecrets() } val exportList = flowOf { buildSecrets() }
@@ -31,6 +33,11 @@ class ManualBackupAdvancedSecretsViewModel(
router.exportJsonAction(payload.toExportPayload()) router.exportJsonAction(payload.toExportPayload())
} }
fun copyMnemonic(mnemonicString: String) {
clipboardManager.addToClipboard(mnemonicString)
showToast(resourceManager.getString(R.string.common_copied))
}
fun backClicked() { fun backClicked() {
router.back() router.back()
} }
@@ -8,6 +8,7 @@ import dagger.Provides
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.resources.ClipboardManager
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter import io.novafoundation.nova.feature_account_impl.presentation.AccountRouter
import io.novafoundation.nova.feature_account_impl.presentation.manualBackup.secrets.advanced.ManualBackupAdvancedSecretsViewModel import io.novafoundation.nova.feature_account_impl.presentation.manualBackup.secrets.advanced.ManualBackupAdvancedSecretsViewModel
@@ -24,13 +25,15 @@ class ManualBackupAdvancedSecretsModule {
resourceManager: ResourceManager, resourceManager: ResourceManager,
router: AccountRouter, router: AccountRouter,
payload: ManualBackupCommonPayload, payload: ManualBackupCommonPayload,
secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory,
clipboardManager: ClipboardManager
): ViewModel { ): ViewModel {
return ManualBackupAdvancedSecretsViewModel( return ManualBackupAdvancedSecretsViewModel(
resourceManager, resourceManager,
router, router,
payload, payload,
secretsAdapterItemFactory secretsAdapterItemFactory,
clipboardManager
) )
} }
@@ -8,4 +8,6 @@ interface ManualBackupItemHandler {
fun onExportJsonClick(item: ManualBackupJsonRvItem) fun onExportJsonClick(item: ManualBackupJsonRvItem)
fun onTapToRevealClicked(item: ManualBackupSecretsVisibilityRvItem) fun onTapToRevealClicked(item: ManualBackupSecretsVisibilityRvItem)
fun onMnemonicCopyClicked(mnemonicString: String)
} }
@@ -25,5 +25,6 @@ class ManualBackupMnemonicViewHolder(private val binder: ItemManualBackupMnemoni
binder.manualBackupSecretsMnemonic.setWordsString(item.mnemonic) binder.manualBackupSecretsMnemonic.setWordsString(item.mnemonic)
binder.manualBackupSecretsMnemonic.showContent(item.isShown) binder.manualBackupSecretsMnemonic.showContent(item.isShown)
binder.manualBackupSecretsMnemonic.onContentShownListener { itemHandler.onTapToRevealClicked(item) } binder.manualBackupSecretsMnemonic.onContentShownListener { itemHandler.onTapToRevealClicked(item) }
binder.manualBackupSecretsMnemonic.setOnCopyClickListener { itemHandler.onMnemonicCopyClicked(it) }
} }
} }
@@ -74,4 +74,8 @@ class ManualBackupSecretsFragment : BaseFragment<ManualBackupSecretsViewModel, F
override fun onTapToRevealClicked(item: ManualBackupSecretsVisibilityRvItem) { override fun onTapToRevealClicked(item: ManualBackupSecretsVisibilityRvItem) {
viewModel.onTapToRevealClicked(item) viewModel.onTapToRevealClicked(item)
} }
override fun onMnemonicCopyClicked(mnemonicString: String) {
viewModel.copyMnemonic(mnemonicString)
}
} }
@@ -1,6 +1,7 @@
package io.novafoundation.nova.feature_account_impl.presentation.manualBackup.secrets.main package io.novafoundation.nova.feature_account_impl.presentation.manualBackup.secrets.main
import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.resources.ClipboardManager
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.flowOf
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
@@ -23,7 +24,8 @@ class ManualBackupSecretsViewModel(
private val accountInteractor: AccountInteractor, private val accountInteractor: AccountInteractor,
private val commonExportSecretsInteractor: CommonExportSecretsInteractor, private val commonExportSecretsInteractor: CommonExportSecretsInteractor,
private val secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory, private val secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory,
private val walletUiUseCase: WalletUiUseCase private val walletUiUseCase: WalletUiUseCase,
private val clipboardManager: ClipboardManager
) : BaseViewModel() { ) : BaseViewModel() {
val walletModel = walletUiUseCase.walletUiFlowFor(payload.metaId, payload.getChainIdOrNull(), showAddressIcon = true) val walletModel = walletUiUseCase.walletUiFlowFor(payload.metaId, payload.getChainIdOrNull(), showAddressIcon = true)
@@ -47,6 +49,11 @@ class ManualBackupSecretsViewModel(
router.exportJsonAction(payload.toExportPayload()) router.exportJsonAction(payload.toExportPayload())
} }
fun copyMnemonic(mnemonicString: String) {
clipboardManager.addToClipboard(mnemonicString)
showToast(resourceManager.getString(R.string.common_copied))
}
fun backClicked() { fun backClicked() {
router.back() router.back()
} }
@@ -8,6 +8,7 @@ import dagger.Provides
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.resources.ClipboardManager
import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
@@ -30,7 +31,8 @@ class ManualBackupSecretsModule {
accountInteractor: AccountInteractor, accountInteractor: AccountInteractor,
commonExportSecretsInteractor: CommonExportSecretsInteractor, commonExportSecretsInteractor: CommonExportSecretsInteractor,
secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory, secretsAdapterItemFactory: ManualBackupSecretsAdapterItemFactory,
walletUiUseCase: WalletUiUseCase walletUiUseCase: WalletUiUseCase,
clipboardManager: ClipboardManager
): ViewModel { ): ViewModel {
return ManualBackupSecretsViewModel( return ManualBackupSecretsViewModel(
resourceManager, resourceManager,
@@ -39,7 +41,8 @@ class ManualBackupSecretsModule {
accountInteractor, accountInteractor,
commonExportSecretsInteractor, commonExportSecretsInteractor,
secretsAdapterItemFactory, secretsAdapterItemFactory,
walletUiUseCase walletUiUseCase,
clipboardManager
) )
} }
@@ -25,6 +25,8 @@ class MnemonicCardView @JvmOverloads constructor(
private val adapter = BackupMnemonicAdapter(this) private val adapter = BackupMnemonicAdapter(this)
private var wordClickedListener: BackupMnemonicAdapter.ItemHandler? = null private var wordClickedListener: BackupMnemonicAdapter.ItemHandler? = null
private var onCopyClickListener: ((String) -> Unit)? = null
private var currentWords: List<String> = emptyList()
private val binder = ViewMnemonicCardViewBinding.inflate(inflater(), this) private val binder = ViewMnemonicCardViewBinding.inflate(inflater(), this)
@@ -38,6 +40,12 @@ class MnemonicCardView @JvmOverloads constructor(
binder.mnemonicCardPhrase.setItemPadding(4.dp) binder.mnemonicCardPhrase.setItemPadding(4.dp)
binder.mnemonicCardPhrase.adapter = adapter binder.mnemonicCardPhrase.adapter = adapter
binder.mnemonicCardCopy.setOnClickListener {
if (currentWords.isNotEmpty()) {
onCopyClickListener?.invoke(currentWords.joinToString(" "))
}
}
binder.mnemonicCardTitle.text = SpannableFormatter.format( binder.mnemonicCardTitle.text = SpannableFormatter.format(
context.getString(R.string.mnemonic_card_title), context.getString(R.string.mnemonic_card_title),
context.getString(R.string.mnemonic_card_title_highlight) context.getString(R.string.mnemonic_card_title_highlight)
@@ -52,10 +60,12 @@ class MnemonicCardView @JvmOverloads constructor(
} }
fun setWords(words: List<MnemonicWord>) { fun setWords(words: List<MnemonicWord>) {
currentWords = words.map { it.content }
adapter.submitList(words) adapter.submitList(words)
} }
fun setWordsString(list: List<String>) { fun setWordsString(list: List<String>) {
currentWords = list
val words = list.mapIndexed { index, item -> val words = list.mapIndexed { index, item ->
MnemonicWord( MnemonicWord(
id = index, id = index,
@@ -64,7 +74,11 @@ class MnemonicCardView @JvmOverloads constructor(
removed = false removed = false
) )
} }
setWords(words) adapter.submitList(words)
}
fun setOnCopyClickListener(listener: ((String) -> Unit)?) {
onCopyClickListener = listener
} }
fun setWordClickedListener(listener: BackupMnemonicAdapter.ItemHandler?) { fun setWordClickedListener(listener: BackupMnemonicAdapter.ItemHandler?) {
@@ -15,13 +15,32 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="12dp"> android:padding="12dp">
<TextView <LinearLayout
android:id="@+id/mnemonicCardTitle"
style="@style/TextAppearance.NovaFoundation.SemiBold.SubHeadline"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/text_secondary" android:orientation="horizontal"
tools:text="Please do not share with anyone" /> android:gravity="center_vertical">
<TextView
android:id="@+id/mnemonicCardTitle"
style="@style/TextAppearance.NovaFoundation.SemiBold.SubHeadline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/text_secondary"
tools:text="Please do not share with anyone" />
<ImageView
android:id="@+id/mnemonicCardCopy"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_copy_outline"
app:tint="@color/icon_secondary"
android:contentDescription="@string/common_copied" />
</LinearLayout>
<io.novafoundation.nova.feature_account_impl.presentation.mnemonic.confirm.view.MnemonicContainerView <io.novafoundation.nova.feature_account_impl.presentation.mnemonic.confirm.view.MnemonicContainerView
android:id="@+id/mnemonicCardPhrase" android:id="@+id/mnemonicCardPhrase"