feature(crypto): verification violation handling and block sending
This commit is contained in:
parent
ce1c01e626
commit
52c57d4d8e
13 changed files with 224 additions and 14 deletions
|
|
@ -10,5 +10,6 @@ package io.element.android.features.messages.impl.crypto.identity
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
sealed interface IdentityChangeEvent {
|
||||
data class Submit(val userId: UserId) : IdentityChangeEvent
|
||||
data class PinViolation(val userId: UserId) : IdentityChangeEvent
|
||||
data class VerificationViolation(val userId: UserId) : IdentityChangeEvent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,12 @@ class IdentityChangeStatePresenter @Inject constructor(
|
|||
|
||||
fun handleEvent(event: IdentityChangeEvent) {
|
||||
when (event) {
|
||||
is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId)
|
||||
is IdentityChangeEvent.VerificationViolation -> {
|
||||
coroutineScope.withdrawVerificationRequirement(event.userId)
|
||||
}
|
||||
is IdentityChangeEvent.PinViolation -> {
|
||||
coroutineScope.pinUserIdentity(event.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,15 +99,22 @@ class IdentityChangeStatePresenter @Inject constructor(
|
|||
Timber.e(it, "Failed to pin identity for user $userId")
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.withdrawVerificationRequirement(userId: UserId) = launch {
|
||||
encryptionService.withdrawVerificationRequirement(userId)
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to withdraw verification for user $userId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
||||
fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
||||
userId = userId,
|
||||
displayNameOrDefault = displayNameOrDefault,
|
||||
avatarData = getAvatarData(AvatarSize.ComposerAlert),
|
||||
)
|
||||
|
||||
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
||||
fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
||||
userId = userId,
|
||||
displayNameOrDefault = userId.extractedDisplayName,
|
||||
avatarData = AvatarData(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState
|
|||
roomMemberIdentityStateChanges = listOf(
|
||||
aRoomMemberIdentityStateChange(
|
||||
identityRoomMember = anIdentityRoomMember(displayNameOrDefault = "Alice"),
|
||||
identityState = IdentityState.PinViolation,
|
||||
identityState = IdentityState.VerificationViolation,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -32,7 +33,8 @@ fun IdentityChangeStateView(
|
|||
// Pick the first identity change to PinViolation
|
||||
val pinViolationIdentityChange = state.roomMemberIdentityStateChanges.firstOrNull {
|
||||
// For now only render PinViolation
|
||||
it.identityState == IdentityState.PinViolation
|
||||
it.identityState == IdentityState.PinViolation ||
|
||||
it.identityState == IdentityState.VerificationViolation
|
||||
}
|
||||
if (pinViolationIdentityChange != null) {
|
||||
ComposerAlertMolecule(
|
||||
|
|
@ -45,12 +47,22 @@ fun IdentityChangeStateView(
|
|||
CommonStrings.crypto_identity_change_pin_violation_new_user_id,
|
||||
pinViolationIdentityChange.identityRoomMember.userId,
|
||||
)
|
||||
val fullText = stringResource(
|
||||
id = CommonStrings.crypto_identity_change_pin_violation_new,
|
||||
displayName,
|
||||
userIdStr,
|
||||
learnMoreStr,
|
||||
)
|
||||
|
||||
val fullText = if (pinViolationIdentityChange.identityState == IdentityState.PinViolation) {
|
||||
stringResource(
|
||||
id = CommonStrings.crypto_identity_change_pin_violation_new,
|
||||
displayName,
|
||||
userIdStr,
|
||||
learnMoreStr,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id = CommonStrings.crypto_identity_change_verification_violation_new,
|
||||
displayName,
|
||||
userIdStr,
|
||||
learnMoreStr,
|
||||
)
|
||||
}
|
||||
append(fullText)
|
||||
val userIdStartIndex = fullText.indexOf(userIdStr)
|
||||
addStyle(
|
||||
|
|
@ -65,6 +77,7 @@ fun IdentityChangeStateView(
|
|||
style = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = ElementTheme.colors.textPrimary
|
||||
),
|
||||
start = learnMoreStartIndex,
|
||||
end = learnMoreStartIndex + learnMoreStr.length,
|
||||
|
|
@ -80,7 +93,18 @@ fun IdentityChangeStateView(
|
|||
end = learnMoreStartIndex + learnMoreStr.length,
|
||||
)
|
||||
},
|
||||
onSubmitClick = { state.eventSink(IdentityChangeEvent.Submit(pinViolationIdentityChange.identityRoomMember.userId)) },
|
||||
submitText = if (pinViolationIdentityChange.identityState == IdentityState.VerificationViolation) {
|
||||
stringResource(CommonStrings.crypto_identity_change_withdraw_verification_action)
|
||||
} else {
|
||||
stringResource(CommonStrings.action_ok)
|
||||
},
|
||||
onSubmitClick = {
|
||||
if (pinViolationIdentityChange.identityState == IdentityState.VerificationViolation) {
|
||||
state.eventSink(IdentityChangeEvent.VerificationViolation(pinViolationIdentityChange.identityRoomMember.userId))
|
||||
} else {
|
||||
state.eventSink(IdentityChangeEvent.PinViolation(pinViolationIdentityChange.identityRoomMember.userId))
|
||||
}
|
||||
},
|
||||
isCritical = pinViolationIdentityChange.identityState == IdentityState.VerificationViolation,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.textcomposer.R
|
||||
|
||||
@Composable
|
||||
internal fun DisabledComposerView(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(4.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.size(48.dp),
|
||||
enabled = false,
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(30.dp),
|
||||
resourceId = CommonDrawables.ic_plus_composer,
|
||||
contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment),
|
||||
tint = ElementTheme.colors.iconDisabled,
|
||||
)
|
||||
}
|
||||
|
||||
val bgColor = ElementTheme.colors.bgCanvasDisabled
|
||||
val borderColor = ElementTheme.colors.borderDisabled
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(21.dp))
|
||||
.border(0.5.dp, borderColor, RoundedCornerShape(21.dp))
|
||||
.background(color = bgColor)
|
||||
.size(42.dp)
|
||||
.requiredHeightIn(min = 42.dp)
|
||||
.weight(1f),
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.size(48.dp),
|
||||
enabled = false,
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(30.dp),
|
||||
imageVector = CompoundIcons.SendSolid(),
|
||||
contentDescription = "",
|
||||
tint = ElementTheme.colors.iconQuaternary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DisabledMessageComposerViewVoicePreview() = ElementPreview {
|
||||
Column {
|
||||
DisabledComposerView(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,10 +14,12 @@ import androidx.annotation.VisibleForTesting
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.ProduceStateScope
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
|
|
@ -33,6 +35,9 @@ import im.vector.app.features.analytics.plan.Interaction
|
|||
import io.element.android.features.messages.impl.MessagesNavigator
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange
|
||||
import io.element.android.features.messages.impl.crypto.identity.createDefaultRoomMemberForIdentityChange
|
||||
import io.element.android.features.messages.impl.crypto.identity.toIdentityRoomMember
|
||||
import io.element.android.features.messages.impl.draft.ComposerDraftService
|
||||
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
|
||||
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
|
||||
|
|
@ -52,6 +57,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||
|
|
@ -76,18 +82,24 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import io.element.android.wysiwyg.display.TextDisplay
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
@ -389,6 +401,11 @@ class MessageComposerPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val roomMemberIdentityStateChange by produceState(persistentListOf()) {
|
||||
observeRoomMemberIdentityStateChange()
|
||||
}
|
||||
|
||||
return MessageComposerState(
|
||||
textEditorState = textEditorState,
|
||||
isFullScreen = isFullScreen.value,
|
||||
|
|
@ -398,6 +415,7 @@ class MessageComposerPresenter @AssistedInject constructor(
|
|||
canShareLocation = canShareLocation.value,
|
||||
canCreatePoll = canCreatePoll.value,
|
||||
suggestions = suggestions.toPersistentList(),
|
||||
roomMemberIdentityStateChanges = roomMemberIdentityStateChange,
|
||||
resolveMentionDisplay = resolveMentionDisplay,
|
||||
eventSink = { handleEvents(it) },
|
||||
)
|
||||
|
|
@ -705,4 +723,33 @@ class MessageComposerPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange() {
|
||||
room.syncUpdateFlow
|
||||
.filter {
|
||||
// Room cannot become unencrypted, so we can just apply a filter here.
|
||||
room.isEncrypted
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState ->
|
||||
identityStateChanges.map { identityStateChange ->
|
||||
val member = membersState.roomMembers()
|
||||
?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }
|
||||
?.toIdentityRoomMember()
|
||||
?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId)
|
||||
RoomMemberIdentityStateChange(
|
||||
identityRoomMember = member,
|
||||
identityState = identityStateChange.identityState,
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { roomMemberIdentityStateChanges ->
|
||||
value = roomMemberIdentityStateChanges.toPersistentList()
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
|
|
@ -24,6 +25,7 @@ data class MessageComposerState(
|
|||
val canShareLocation: Boolean,
|
||||
val canCreatePoll: Boolean,
|
||||
val suggestions: ImmutableList<ResolvedSuggestion>,
|
||||
val roomMemberIdentityStateChanges: ImmutableList<RoomMemberIdentityStateChange>,
|
||||
val resolveMentionDisplay: (String, String) -> TextDisplay,
|
||||
val eventSink: (MessageComposerEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
|
|
@ -32,6 +33,7 @@ fun aMessageComposerState(
|
|||
canShareLocation: Boolean = true,
|
||||
canCreatePoll: Boolean = true,
|
||||
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
|
||||
identityStates: ImmutableList<RoomMemberIdentityStateChange> = persistentListOf(),
|
||||
eventSink: (MessageComposerEvents) -> Unit = {},
|
||||
) = MessageComposerState(
|
||||
textEditorState = textEditorState,
|
||||
|
|
@ -42,6 +44,7 @@ fun aMessageComposerState(
|
|||
canShareLocation = canShareLocation,
|
||||
canCreatePoll = canCreatePoll,
|
||||
suggestions = suggestions,
|
||||
roomMemberIdentityStateChanges = identityStates,
|
||||
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes
|
|||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.textcomposer.TextComposer
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
|
|
@ -37,6 +38,14 @@ internal fun MessageComposerView(
|
|||
enableVoiceMessages: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val verificationViolation = state.roomMemberIdentityStateChanges.firstOrNull {
|
||||
it.identityState == IdentityState.VerificationViolation
|
||||
}
|
||||
if (verificationViolation != null) {
|
||||
DisabledComposerView(modifier = modifier)
|
||||
return
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
fun sendMessage() {
|
||||
state.eventSink(MessageComposerEvents.SendMessage)
|
||||
|
|
@ -139,6 +148,7 @@ internal fun MessageComposerViewPreview(
|
|||
enableVoiceMessages = true,
|
||||
subcomposing = false,
|
||||
)
|
||||
DisabledComposerView()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,11 @@ fun ComposerAlertMolecule(
|
|||
text = content,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
color = if (isCritical) {
|
||||
ElementTheme.colors.textCriticalPrimary
|
||||
} else {
|
||||
ElementTheme.colors.textPrimary
|
||||
},
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ interface EncryptionService {
|
|||
* Remember this identity, ensuring it does not result in a pin violation.
|
||||
*/
|
||||
suspend fun pinUserIdentity(userId: UserId): Result<Unit>
|
||||
suspend fun withdrawVerificationRequirement(userId: UserId): Result<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -209,6 +209,10 @@ internal class RustEncryptionService(
|
|||
getUserIdentity(userId).pin()
|
||||
}
|
||||
|
||||
override suspend fun withdrawVerificationRequirement(userId: UserId): Result<Unit> = runCatching {
|
||||
getUserIdentity(userId).withdrawVerification()
|
||||
}
|
||||
|
||||
private suspend fun getUserIdentity(userId: UserId): UserIdentity {
|
||||
return service.userIdentity(
|
||||
userId = userId.value,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class FakeEncryptionService(
|
|||
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
|
||||
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
|
||||
private val isUserVerifiedResult: (UserId) -> Result<Boolean> = { lambdaError() },
|
||||
private val withdrawVerificationResult: (UserId) -> Result<Unit> = { lambdaError() },
|
||||
) : EncryptionService {
|
||||
private var disableRecoveryFailure: Exception? = null
|
||||
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
|
||||
|
|
@ -124,6 +125,10 @@ class FakeEncryptionService(
|
|||
return pinUserIdentityResult(userId)
|
||||
}
|
||||
|
||||
override suspend fun withdrawVerificationRequirement(userId: UserId): Result<Unit> {
|
||||
return withdrawVerificationResult(userId)
|
||||
}
|
||||
|
||||
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = simulateLongTask {
|
||||
isUserVerifiedResult(userId)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue