fix: Copy protected field behind UV #2283

This commit is contained in:
J-Jamet
2025-12-03 15:04:35 +01:00
parent eb41233e57
commit 3567fa797b
11 changed files with 221 additions and 89 deletions

View File

@@ -56,10 +56,11 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TagsAdapter
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestUnprotectField
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestShowUnprotectField
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
@@ -330,11 +331,26 @@ class EntryActivity : DatabaseLockActivity() {
when (entryState) {
is EntryViewModel.EntryState.Loading -> {}
is EntryViewModel.EntryState.RequestUnprotectField -> {
requestUnprotectField(
mDatabase?.let { database ->
requestShowUnprotectField(
userVerificationViewModel = mUserVerificationViewModel,
database = mDatabase,
database = database,
protectedFieldView = entryState.protectedFieldView
)
}
mEntryViewModel.actionPerformed()
}
is EntryViewModel.EntryState.RequestCopyProtectedField -> {
mDatabase?.let { database ->
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.COPY_PROTECTED_FIELD,
database = database,
field = entryState.field,
)
)
}
mEntryViewModel.actionPerformed()
}
}
@@ -351,10 +367,24 @@ class EntryActivity : DatabaseLockActivity() {
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
// Edit Entry if corresponding data
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
// Unprotect field if corresponding data
uVState.dataToVerify.protectedFieldView?.unprotect()
val data = uVState.dataToVerify
when (data.actionType) {
UserVerificationActionType.SHOW_PROTECTED_FIELD -> {
// Unprotect field by its view
data.protectedFieldView?.unprotect()
}
UserVerificationActionType.COPY_PROTECTED_FIELD -> {
// Copy field value
data.field?.let {
mEntryViewModel.copyToClipboard(it)
}
}
UserVerificationActionType.EDIT_ENTRY -> {
// Edit Entry
editEntry(data.database, data.entryId)
}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
@@ -531,10 +561,16 @@ class EntryActivity : DatabaseLockActivity() {
when (item.itemId) {
R.id.menu_edit -> {
if (mEntryViewModel.entryInfo?.isUserVerificationNeeded() == true) {
mDatabase?.let { database ->
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId)
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.EDIT_ENTRY,
database = database,
entryId = mEntryViewModel.mainEntryId
)
)
}
} else {
editEntry(mDatabase, mEntryViewModel.mainEntryId)
}

View File

@@ -63,7 +63,8 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildSpecia
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveRegisterInfo
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestUnprotectField
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.requestShowUnprotectField
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
@@ -403,11 +404,13 @@ class EntryEditActivity : DatabaseLockActivity(),
mEntryEditViewModel.actionPerformed()
}
is EntryEditViewModel.EntryEditState.RequestUnprotectField -> {
requestUnprotectField(
mDatabase?.let { database ->
requestShowUnprotectField(
userVerificationViewModel = mUserVerificationViewModel,
database = mDatabase,
database = database,
protectedFieldView = uiState.protectedFieldView
)
}
mEntryEditViewModel.actionPerformed()
}
}
@@ -424,7 +427,12 @@ class EntryEditActivity : DatabaseLockActivity(),
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
when (uVState.dataToVerify.actionType) {
UserVerificationActionType.SHOW_PROTECTED_FIELD -> {
uVState.dataToVerify.protectedFieldView?.unprotect()
}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}

View File

@@ -75,6 +75,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
@@ -586,7 +587,15 @@ class GroupActivity : DatabaseLockActivity(),
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
val data = uVState.dataToVerify
when (data.actionType) {
UserVerificationActionType.EDIT_ENTRY -> {
editEntry(uVState.dataToVerify.database, uVState.dataToVerify.entryId)
}
UserVerificationActionType.MERGE_FROM_DATABASE -> {}
UserVerificationActionType.SAVE_TO_DATABASE -> {}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
@@ -1101,7 +1110,11 @@ class GroupActivity : DatabaseLockActivity(),
if ((node as Entry).getEntryInfo(database).isUserVerificationNeeded()) {
checkUserVerification(
userVerificationViewModel = mUserVerificationViewModel,
dataToVerify = UserVerificationData(database, node.nodeId)
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.EDIT_ENTRY,
database = database,
entryId = node.nodeId
)
)
} else {
editEntry(database, node.nodeId)

View File

@@ -16,13 +16,10 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
import com.kunzisoft.keepass.view.TemplateView
@@ -50,8 +47,6 @@ class EntryFragment: DatabaseFragment() {
private lateinit var uuidContainerView: View
private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null
private val mEntryViewModel: EntryViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
@@ -66,10 +61,6 @@ class EntryFragment: DatabaseFragment() {
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { context ->
mClipboardHelper = ClipboardHelper(context)
}
rootView = view
// Hide only the first time
if (savedInstanceState == null) {
@@ -152,18 +143,14 @@ class EntryFragment: DatabaseFragment() {
private fun assignEntryInfo(entryInfo: EntryInfo?) {
// Set copy buttons
templateView.apply {
setOnUnprotectClickListener { _, textEditFieldView ->
mEntryViewModel.requestUnprotectField(textEditFieldView)
setOnUnprotectClickListener { protectedFieldView ->
mEntryViewModel.requestUnprotectField(protectedFieldView)
}
setOnAskCopySafeClickListener {
showClipboardDialog()
}
setOnCopyActionClickListener { field ->
mClipboardHelper?.timeoutCopyToClipboard(
TemplateField.getLocalizedName(context, field.name),
field.protectedValue.stringValue,
field.protectedValue.isProtected
)
setOnCopyActionClickListener { field, protectedFieldView ->
mEntryViewModel.requestCopyField(field, protectedFieldView)
}
}
@@ -251,7 +238,7 @@ class EntryFragment: DatabaseFragment() {
fun launchEntryCopyEducationAction() {
val appNameString = getString(R.string.app_name)
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
mEntryViewModel.copyToClipboard(appNameString)
}
companion object {

View File

@@ -1,12 +1,25 @@
package com.kunzisoft.keepass.credentialprovider
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.view.ProtectedFieldView
data class UserVerificationData(
val actionType: UserVerificationActionType,
val database: ContextualDatabase? = null,
val entryId: NodeId<*>? = null,
val field: Field? = null,
val protectedFieldView: ProtectedFieldView? = null,
val preferenceKey: String? = null
)
enum class UserVerificationActionType {
LAUNCH_PASSKEY_CEREMONY,
SHOW_PROTECTED_FIELD,
COPY_PROTECTED_FIELD,
EDIT_ENTRY,
EDIT_DATABASE_SETTING,
MERGE_FROM_DATABASE,
SAVE_TO_DATABASE
}

View File

@@ -104,15 +104,16 @@ class UserVerificationHelper {
activity?.checkUserVerification(userVerificationViewModel, dataToVerify)
}
fun FragmentActivity.requestUnprotectField(
fun FragmentActivity.requestShowUnprotectField(
userVerificationViewModel: UserVerificationViewModel,
database: ContextualDatabase?,
database: ContextualDatabase,
protectedFieldView: ProtectedFieldView
) {
if (protectedFieldView.isCurrentlyProtected()) {
checkUserVerification(
userVerificationViewModel = userVerificationViewModel,
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.SHOW_PROTECTED_FIELD,
database = database,
protectedFieldView = protectedFieldView
)

View File

@@ -45,6 +45,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
@@ -179,12 +180,18 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
when (uiState) {
is UserVerificationViewModel.UIState.Loading -> {}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
val data = uiState.dataToVerify
when (data.actionType) {
UserVerificationActionType.LAUNCH_PASSKEY_CEREMONY -> {
passkeyLauncherViewModel.launchActionIfNeeded(
userVerified = true,
intent = intent,
specialMode = mSpecialMode,
database = uiState.dataToVerify.database
)
}
else -> {}
}
userVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> {
@@ -207,7 +214,10 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
if (userVerificationNeeded) {
checkUserVerification(
userVerificationViewModel = userVerificationViewModel,
dataToVerify = UserVerificationData(database)
dataToVerify = UserVerificationData(
actionType = UserVerificationActionType.LAUNCH_PASSKEY_CEREMONY,
database = database
)
)
} else {
passkeyLauncherViewModel.launchActionIfNeeded(

View File

@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.credentialprovider.UserVerificationActionType
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -188,8 +189,12 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mUserVerificationViewModel.onUserVerificationReceived()
}
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
state.dataToVerify.database?.let { database ->
state.dataToVerify.preferenceKey?.let { preferenceKey ->
val data = state.dataToVerify
when (data.actionType) {
UserVerificationActionType.EDIT_DATABASE_SETTING -> {
val database = data.database
val preferenceKey = data.preferenceKey
if (database != null && preferenceKey != null) {
// Main Preferences
when (preferenceKey) {
// Master Key
@@ -211,6 +216,8 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mSettingsViewModel.dialogFragment = null
}
}
else -> {}
}
mUserVerificationViewModel.onUserVerificationReceived()
}
}
@@ -483,6 +490,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
checkUserVerification(
mUserVerificationViewModel,
UserVerificationData(
actionType = UserVerificationActionType.EDIT_DATABASE_SETTING,
database = database,
preferenceKey = changeCredentialKey
)
@@ -513,7 +521,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
// To reassign color listener after orientation change
val chromaDialog = parentFragmentManager.findFragmentByTag(TAG_PREF_FRAGMENT) as DatabaseColorPreferenceDialogFragmentCompat?
chromaDialog?.onColorSelectedListener = colorSelectedListener
} catch (e: Exception) {}
} catch (_: Exception) {}
return view
}
@@ -785,6 +793,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
checkUserVerification(
mUserVerificationViewModel,
UserVerificationData(
actionType = UserVerificationActionType.EDIT_DATABASE_SETTING,
database = mDatabase,
preferenceKey = preference.key
)

View File

@@ -25,8 +25,8 @@ class TemplateView @JvmOverloads constructor(context: Context,
: TemplateAbstractView<TextFieldView, TextFieldView, DateTimeFieldView>
(context, attrs, defStyle) {
private var mOnUnprotectClickListener: ((Field, ProtectedFieldView) -> Unit)? = null
fun setOnUnprotectClickListener(listener: ((Field, ProtectedFieldView) -> Unit)?) {
private var mOnUnprotectClickListener: ((ProtectedFieldView) -> Unit)? = null
fun setOnUnprotectClickListener(listener: ((ProtectedFieldView) -> Unit)?) {
this.mOnUnprotectClickListener = listener
}
@@ -34,8 +34,8 @@ class TemplateView @JvmOverloads constructor(context: Context,
fun setOnAskCopySafeClickListener(listener: (() -> Unit)? = null) {
this.mOnAskCopySafeClickListener = listener
}
private var mOnCopyActionClickListener: ((Field) -> Unit)? = null
fun setOnCopyActionClickListener(listener: ((Field) -> Unit)? = null) {
private var mOnCopyActionClickListener: ((Field, ProtectedFieldView) -> Unit)? = null
fun setOnCopyActionClickListener(listener: ((Field, ProtectedFieldView) -> Unit)? = null) {
this.mOnCopyActionClickListener = listener
}
@@ -64,7 +64,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected) {
mOnUnprotectClickListener?.invoke(field, this)
mOnUnprotectClickListener?.invoke(this)
}
label = templateAttribute.alias
?: TemplateField.getLocalizedName(context, field.name)
@@ -85,7 +85,15 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(true, value)))
?.invoke(
Field(
name = label,
value = ProtectedString(
enableProtection = true,
string = value
)
), this
)
}
} else {
setCopyButtonState(TextFieldView.ButtonState.GONE)
@@ -96,7 +104,15 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(false, value)))
?.invoke(
Field(
name = label,
value = ProtectedString(
enableProtection = false,
string = value
)
), this
)
}
}
}
@@ -184,9 +200,15 @@ class TemplateView @JvmOverloads constructor(context: Context,
value = otpElement.tokenString
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { _, _ ->
mOnCopyActionClickListener?.invoke(Field(
otpElement.type.name,
ProtectedString(false, otpElement.token)))
mOnCopyActionClickListener?.invoke(
Field(
name = otpElement.type.name,
value = ProtectedString(
enableProtection = false,
string = otpElement.token
)
), this
)
}
textDirection = TEXT_DIRECTION_LTR
mLastOtpTokenView = this

View File

@@ -19,17 +19,22 @@
*/
package com.kunzisoft.keepass.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.utils.IOActionTask
import com.kunzisoft.keepass.view.ProtectedFieldView
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,7 +42,7 @@ import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
class EntryViewModel: ViewModel() {
class EntryViewModel(application: Application): AndroidViewModel(application) {
var mainEntryId: NodeId<UUID>? = null
private set
@@ -50,6 +55,8 @@ class EntryViewModel: ViewModel() {
var entryLoaded = false
private set
private var mClipboardHelper: ClipboardHelper = ClipboardHelper(application)
val entryInfoHistory : LiveData<EntryInfoHistory?> get() = _entryInfoHistory
private val _entryInfoHistory = MutableLiveData<EntryInfoHistory?>()
@@ -136,6 +143,14 @@ class EntryViewModel: ViewModel() {
mEntryState.value = EntryState.RequestUnprotectField(fieldView)
}
fun requestCopyField(field: Field, fieldView: ProtectedFieldView) {
// Only request the User Verification if the field is protected and not shown
if (field.protectedValue.isProtected && fieldView.isCurrentlyProtected())
mEntryState.value = EntryState.RequestCopyProtectedField(field)
else
copyToClipboard(field)
}
fun onOtpElementUpdated(optElement: OtpElement?) {
_onOtpElementUpdated.value = optElement
}
@@ -156,6 +171,18 @@ class EntryViewModel: ViewModel() {
_sectionSelected.value = section
}
fun copyToClipboard(field: Field) {
mClipboardHelper.timeoutCopyToClipboard(
TemplateField.getLocalizedName(getApplication(), field.name),
field.protectedValue.stringValue,
field.protectedValue.isProtected
)
}
fun copyToClipboard(text: String) {
mClipboardHelper.timeoutCopyToClipboard(text, text)
}
fun actionPerformed() {
mEntryState.value = EntryState.Loading
}
@@ -186,6 +213,9 @@ class EntryViewModel: ViewModel() {
data class RequestUnprotectField(
val protectedFieldView: ProtectedFieldView
): EntryState()
data class RequestCopyProtectedField(
val field: Field
): EntryState()
}
companion object {

View File

@@ -15,16 +15,17 @@ class UserVerificationViewModel: ViewModel() {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val userVerificationState: StateFlow<UIState> = mUiState
var dataToVerify: UserVerificationData = UserVerificationData()
var dataToVerify: UserVerificationData? = null
fun checkMainCredential(checkString: String) {
// Check the password part
if (dataToVerify.database?.checkKey(getCheckKey(checkString)) == true)
onUserVerificationSucceeded(dataToVerify)
val data = dataToVerify
if (data?.database?.checkKey(getCheckKey(checkString)) == true)
onUserVerificationSucceeded(data)
else {
onUserVerificationFailed(dataToVerify, InvalidCredentialsDatabaseException())
}
dataToVerify = UserVerificationData()
dataToVerify = null
}
fun onUserVerificationSucceeded(dataToVerify: UserVerificationData) {
@@ -32,7 +33,7 @@ class UserVerificationViewModel: ViewModel() {
}
fun onUserVerificationFailed(
dataToVerify: UserVerificationData = UserVerificationData(),
dataToVerify: UserVerificationData? = null,
error: Throwable? = null
) {
this.dataToVerify = dataToVerify
@@ -45,9 +46,11 @@ class UserVerificationViewModel: ViewModel() {
sealed class UIState {
object Loading: UIState()
data class OnUserVerificationSucceeded(val dataToVerify: UserVerificationData): UIState()
data class OnUserVerificationSucceeded(
val dataToVerify: UserVerificationData
): UIState()
data class OnUserVerificationCanceled(
val dataToVerify: UserVerificationData,
val dataToVerify: UserVerificationData?,
val error: Throwable?
): UIState()
}