diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt index 64bd83c6c0..25010efa83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt @@ -2,59 +2,44 @@ package org.thoughtcrime.securesms.verify import android.content.ActivityNotFoundException import android.content.Context -import android.content.DialogInterface import android.content.Intent -import android.content.res.ColorStateList import android.os.Bundle -import android.text.TextUtils import android.view.ContextMenu import android.view.ContextMenu.ContextMenuInfo -import android.view.LayoutInflater import android.view.MenuItem import android.view.View -import android.view.ViewGroup import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.core.widget.ImageViewCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.util.ThreadUtil -import org.signal.core.util.Util -import org.signal.core.util.logging.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.signal.core.ui.compose.ComposeFragment import org.signal.core.util.requireParcelableCompat -import org.signal.libsignal.protocol.fingerprint.Fingerprint import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.components.verify.SafetyNumberQrView.Companion.getSegments import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable -import org.thoughtcrime.securesms.databinding.VerifyDisplayFragmentBinding -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.util.RemoteConfig -import org.thoughtcrime.securesms.util.ViewUtil -import org.thoughtcrime.securesms.util.visible -import java.nio.charset.StandardCharsets -import java.util.Locale -import org.signal.core.ui.R as CoreUiR +import org.thoughtcrime.securesms.util.CommunicationActions /** * Fragment to display a user's identity key. */ -class VerifyDisplayFragment : Fragment() { - private lateinit var viewModel: VerifySafetyNumberViewModel +class VerifyDisplayFragment : ComposeFragment() { + private val viewModel: VerifyDisplayScreenViewModel by viewModels { + val isVerified = requireArguments().getBoolean(VERIFIED_STATE, false) + val recipientId = requireArguments().requireParcelableCompat(RECIPIENT_ID, RecipientId::class.java) + val localIdentity = requireArguments().requireParcelableCompat(LOCAL_IDENTITY, IdentityKeyParcelable::class.java).get()!! + val remoteIdentity = requireArguments().requireParcelableCompat(REMOTE_IDENTITY, IdentityKeyParcelable::class.java).get()!! - private val binding by ViewBinderDelegate(VerifyDisplayFragmentBinding::bind) + VerifyDisplayScreenViewModel.Factory( + isSafetyNumberVerified = isVerified, + recipientId = recipientId, + localIdentity = localIdentity, + remoteIdentity = remoteIdentity + ) + } private var callback: Callback? = null - private var animateCodeChanges = true - - private var animateSuccessOnDraw = false - private var animateFailureOnDraw = false - private var currentVerifiedState = false - override fun onAttach(context: Context) { super.onAttach(context) callback = if (context is Callback) { @@ -66,145 +51,15 @@ class VerifyDisplayFragment : Fragment() { } } - override fun onCreateView(inflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? { - return ViewUtil.inflate(inflater, viewGroup!!, R.layout.verify_display_fragment) - } + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - initializeViewModel() - - if (RemoteConfig.internalUser && SignalStore.settings.automaticVerificationEnabled && !SignalStore.uiHints.hasSeenVerifyAutomaticallySheet()) { - VerifyAutomaticallyEducationSheet.show(parentFragmentManager) - } - - updateVerifyButton(requireArguments().getBoolean(VERIFIED_STATE, false), false) - - binding.automaticVerification.visible = RemoteConfig.internalUser && SignalStore.settings.automaticVerificationEnabled - binding.safetyQrView.verifyButton.setOnClickListener { updateVerifyButton(!currentVerifiedState, true) } - binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() } - binding.toolbar.setTitle(R.string.AndroidManifest__verify_safety_number) - - binding.caption.text = getString(R.string.verify_display_fragment__auto_verify_not_available) - binding.caption.setLink(getString(R.string.verify_display_fragment__link)) - binding.caption.setLinkColor(ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorPrimary)) - - viewModel.getAutomaticVerification().observe(viewLifecycleOwner) { status -> - if (status == AutomaticVerificationStatus.NONE) { - binding.autoVerifyContainer.setOnClickListener { - viewModel.verifyAutomatically() - } - } else { - binding.autoVerifyContainer.setOnClickListener(null) - } - animateStatus(status) - } - - viewModel.recipient.observe(this) { recipient: Recipient -> setRecipientText(recipient) } - viewModel.getFingerprint().observe(viewLifecycleOwner) { fingerprint: SafetyNumberFingerprint? -> - if (fingerprint == null) { - val resolved = viewModel.recipient.resolve() - Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", RemoteConfig.verifyV2, resolved.serviceId.isPresent, resolved.e164.isPresent)) - MaterialAlertDialogBuilder(requireContext()) - .setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext()))) - .setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> requireActivity().finish() } - .setOnDismissListener { dialog: DialogInterface -> - requireActivity().finish() - dialog.dismiss() - } - .show() - return@observe - } - - binding.safetyQrView.setFingerprintViews(fingerprint.fingerprint, animateCodeChanges) - binding.safetyQrView.shareButton.setOnClickListener { v: View? -> handleShare(fingerprint.fingerprint) } - binding.safetyQrView.qrCodeContainer.setOnClickListener { v: View? -> callback!!.onQrCodeContainerClicked() } - registerForContextMenu(binding.safetyQrView.numbersContainer) - } - } - - private fun animateStatus(status: AutomaticVerificationStatus) { - if (status == AutomaticVerificationStatus.NONE || status == AutomaticVerificationStatus.UNAVAILABLE_PERMANENT) { - updateStatus(status) - } else { - binding.autoVerifyContainer.animate() - .alpha(0f) - .setDuration(FADE_TIME) - .withEndAction { - updateStatus(status) - - binding.autoVerifyContainer.animate() - .alpha(1f) - .setDuration(FADE_TIME) - .start() - } - .start() - } - } - - private fun updateStatus(status: AutomaticVerificationStatus) { - when (status) { - AutomaticVerificationStatus.NONE -> { - binding.autoVerifyText.text = getString(R.string.verify_display_fragment__verify_automatic) - binding.autoVerifyIcon.setImageResource(R.drawable.symbol_key_24) - binding.autoVerifyIcon.imageTintList = null - binding.autoVerifyMore.visible = false - } - AutomaticVerificationStatus.VERIFYING -> { - binding.autoVerifyText.text = getString(R.string.verify_display_fragment__verifying) - binding.autoVerifyMore.visible = false - } - AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> { - binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_unavailable) - binding.autoVerifyIcon.setImageResource(CoreUiR.drawable.symbol_info_24) - ImageViewCompat.setImageTintList(binding.autoVerifyIcon, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorOnSurfaceVariant))) - binding.autoVerifyMore.visible = true - } - AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY -> { - binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_unavailable) - binding.autoVerifyIcon.setImageResource(CoreUiR.drawable.symbol_info_24) - ImageViewCompat.setImageTintList(binding.autoVerifyIcon, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorOnSurfaceVariant))) - binding.autoVerifyMore.visible = true - } - AutomaticVerificationStatus.VERIFIED -> { - binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_verified) - binding.autoVerifyIcon.setImageResource(R.drawable.symbol_check_filled_circle_24) - binding.autoVerifyIcon.imageTintList = null - binding.autoVerifyMore.visible = true - } - } - - if (status == AutomaticVerificationStatus.VERIFYING) { - binding.autoVerifySpinner.visible = true - binding.autoVerifyIcon.visible = false - } else { - binding.autoVerifySpinner.visible = false - binding.autoVerifyIcon.visible = true - } - binding.autoVerifyMore.setOnClickListener { EncryptionVerifiedSheet.show(parentFragmentManager, status, viewModel.recipient.resolve().getDisplayName(requireContext())) } - } - - private fun initializeViewModel() { - val recipientId = requireArguments().requireParcelableCompat(RECIPIENT_ID, RecipientId::class.java) - val localIdentity = requireArguments().requireParcelableCompat(LOCAL_IDENTITY, IdentityKeyParcelable::class.java).get()!! - val remoteIdentity = requireArguments().requireParcelableCompat(REMOTE_IDENTITY, IdentityKeyParcelable::class.java).get()!! - - viewModel = ViewModelProvider(this, VerifySafetyNumberViewModel.Factory(recipientId, localIdentity, remoteIdentity)).get(VerifySafetyNumberViewModel::class.java) - } - - override fun onResume() { - super.onResume() - setRecipientText(viewModel.recipient.get()) - if (animateSuccessOnDraw) { - animateSuccessOnDraw = false - ThreadUtil.postToMain { - animateSuccess() - } - } else if (animateFailureOnDraw) { - animateFailureOnDraw = false - ThreadUtil.postToMain { - animateFailure() - } - } + VerifyDisplayScreen( + state = state, + emitter = this::onEvent, + onQrViewInitialized = this::onQrViewInitialised + ) } override fun onCreateContextMenu( @@ -213,7 +68,7 @@ class VerifyDisplayFragment : Fragment() { menuInfo: ContextMenuInfo? ) { super.onCreateContextMenu(menu, view, menuInfo) - val fingerprint = viewModel.getFingerprint().value + val fingerprint = viewModel.fingerprintSnapshot if (fingerprint != null) { val inflater = requireActivity().menuInflater inflater.inflate(R.menu.verify_display_fragment_context_menu, menu) @@ -221,126 +76,47 @@ class VerifyDisplayFragment : Fragment() { } override fun onContextItemSelected(item: MenuItem): Boolean { - if (currentFingerprint == null) return super.onContextItemSelected(item) - return if (item.itemId == R.id.menu_copy) { - handleCopyToClipboard(currentFingerprint) - true - } else if (item.itemId == R.id.menu_compare) { - handleCompareWithClipboard() - true - } else { - super.onContextItemSelected(item) + if (viewModel.fingerprintSnapshot == null) return super.onContextItemSelected(item) + return when (item.itemId) { + R.id.menu_copy -> { + viewModel.copyFingerprintToClipboard() + true + } + R.id.menu_compare -> { + viewModel.compareClipboardToFingerprint() + true + } + else -> { + super.onContextItemSelected(item) + } } } - private val currentFingerprint: Fingerprint? - get() { - return viewModel.getFingerprint().value?.fingerprint - } - fun setScannedFingerprint(scanned: String) { - animateCodeChanges = false - - currentFingerprint?.let { - try { - if (currentFingerprint!!.scannableFingerprint.compareTo(scanned.toByteArray(StandardCharsets.ISO_8859_1))) { - animateSuccessOnDraw = true - } else { - animateFailureOnDraw = true - } - } catch (e: Exception) { - Log.w(TAG, e) - showAlertDialog(R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number) - animateFailureOnDraw = true - return - } - } + viewModel.setScannedFingerprint(scanned) } - private fun showAlertDialog(stringResId: Int) { - MaterialAlertDialogBuilder(requireContext()) - .setMessage(stringResId) - .setPositiveButton(R.string.VerifyDisplayFragment__scan_result_dialog_ok, null) - .show() - } - - private fun getFormattedSafetyNumbers(fingerprint: Fingerprint): String { - val segments = getSegments(fingerprint) - val result = StringBuilder() - for (i in segments.indices) { - result.append(segments[i]) - if (i != segments.size - 1) { - if ((i + 1) % 4 == 0) result.append('\n') else result.append(' ') - } - } - return result.toString() - } - - private fun handleCopyToClipboard(fingerprint: Fingerprint?) { - Util.writeTextToClipboard(requireContext(), "Safety numbers", getFormattedSafetyNumbers(fingerprint!!)) - } - - private fun handleCompareWithClipboard() { - val clipboardData = Util.readTextFromClipboard(requireActivity()) - if (clipboardData == null) { - showAlertDialog(R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard) - return - } - val numericClipboardData = clipboardData.replace("\\D".toRegex(), "") - if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length != 60) { - showAlertDialog(R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard) - return - } - if (currentFingerprint?.displayableFingerprint?.displayText == numericClipboardData) { - animateSuccess() - } else { - animateFailure() - } - } - - private fun animateSuccess() { - animateCodeChanges = false - binding.safetyQrView.animateVerifiedSuccess() - } - - private fun animateFailure() { - animateCodeChanges = false - binding.safetyQrView.animateVerifiedFailure() - } - - private fun handleShare(fingerprint: Fingerprint) { - val shareString = """ - ${getString(R.string.VerifyIdentityActivity_our_signal_safety_number)} - ${getFormattedSafetyNumbers(fingerprint)} - - """.trimIndent() - val intent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, shareString) - } + private fun handleShare() { try { - startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via))) + startActivity(Intent.createChooser(viewModel.createShareIntent(requireActivity()), getString(R.string.VerifyIdentityActivity_share_safety_number_via))) } catch (e: ActivityNotFoundException) { Toast.makeText(activity, R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show() } } - private fun setRecipientText(recipient: Recipient) { - binding.description.text = getString(R.string.verify_display_fragment__pnp_verify_safety_numbers_explanation_with_s, recipient.getDisplayName(requireContext())) - binding.description.setLink("https://signal.org/redirect/safety-numbers") - binding.description.setLinkColor(ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorPrimary)) + private fun onQrViewInitialised(view: View) { + registerForContextMenu(view) } - private fun updateVerifyButton(verified: Boolean, update: Boolean) { - currentVerifiedState = verified - if (verified) { - binding.safetyQrView.verifyButton.setText(R.string.verify_display_fragment__clear_verification) - } else { - binding.safetyQrView.verifyButton.setText(R.string.verify_display_fragment__mark_as_verified) - } - if (update) { - viewModel.updateSafetyNumberVerification(verified) + private fun onEvent(event: VerifyDisplayScreenEvent) { + when (event) { + VerifyDisplayScreenEvent.EducationDismiss -> viewModel.setSeenEducationSheet() + VerifyDisplayScreenEvent.EducationLearnMoreClick -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.verify_display_fragment__link)) + VerifyDisplayScreenEvent.QrClick -> callback?.onQrCodeContainerClicked() + VerifyDisplayScreenEvent.ShareClick -> handleShare() + VerifyDisplayScreenEvent.VerifyAutomaticallyClick -> viewModel.verifyAutomatically() + is VerifyDisplayScreenEvent.VerifyButtonClick -> viewModel.updateSafetyNumberVerification(event.isVerified) + VerifyDisplayScreenEvent.YouMustFirstExchangeMessagesDialogDismiss -> requireActivity().finish() } } @@ -349,14 +125,11 @@ class VerifyDisplayFragment : Fragment() { } companion object { - private val TAG = Log.tag(VerifyDisplayFragment::class.java) - private const val RECIPIENT_ID = "recipient_id" private const val REMOTE_IDENTITY = "remote_identity" private const val LOCAL_IDENTITY = "local_identity" private const val LOCAL_NUMBER = "local_number" private const val VERIFIED_STATE = "verified_state" - private const val FADE_TIME = 250L fun create( recipientId: RecipientId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayRepository.kt new file mode 100644 index 0000000000..b1f2464fcd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayRepository.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.verify + +import android.content.Context +import android.content.Intent +import androidx.annotation.UiContext +import org.signal.core.util.Util +import org.signal.libsignal.protocol.fingerprint.Fingerprint +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.verify.SafetyNumberQrView.Companion.getSegments +import org.thoughtcrime.securesms.dependencies.AppDependencies + +object VerifyDisplayRepository { + fun writeToClipboard(fingerprint: Fingerprint) { + Util.writeTextToClipboard(AppDependencies.application, "Safety numbers", getFormattedSafetyNumbers(fingerprint)) + } + + fun readFromClipboard(): String? { + return Util.readTextFromClipboard(AppDependencies.application) + } + + @UiContext + fun createShareIntent(context: Context, fingerprint: Fingerprint): Intent { + val shareString = """ + ${context.getString(R.string.VerifyIdentityActivity_our_signal_safety_number)} + ${getFormattedSafetyNumbers(fingerprint)} + + """.trimIndent() + return Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, shareString) + } + } + + private fun getFormattedSafetyNumbers(fingerprint: Fingerprint): String { + val segments = getSegments(fingerprint) + val result = StringBuilder() + for (i in segments.indices) { + result.append(segments[i]) + if (i != segments.size - 1) { + if ((i + 1) % 4 == 0) result.append('\n') else result.append(' ') + } + } + return result.toString() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreen.kt new file mode 100644 index 0000000000..0e836e9b81 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreen.kt @@ -0,0 +1,454 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.verify + +import android.view.View +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withLink +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Dialogs +import org.signal.core.ui.compose.IconButtons.IconButton +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.Scaffolds +import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.horizontalGutters +import org.signal.core.ui.compose.theme.SignalTheme +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.verify.SafetyNumberQrView +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.rememberRecipientField +import org.thoughtcrime.securesms.util.CommunicationActions + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun VerifyDisplayScreen( + state: VerifyDisplayScreenState, + emitter: (VerifyDisplayScreenEvent) -> Unit, + onQrViewInitialized: (View) -> Unit +) { + state.recipient ?: return + + val context = LocalContext.current + val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + val displayName by rememberRecipientField(state.recipient) { getDisplayName(context) } + + Scaffolds.Settings( + title = stringResource(R.string.AndroidManifest__verify_safety_number), + navigationIcon = SignalIcons.ArrowStart.imageVector, + onNavigationClick = { backPressedDispatcher?.onBackPressed() } + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(it) + ) { + SafetyNumberQr( + state = state, + emitter = emitter, + onQrViewInitialized = onQrViewInitialized, + modifier = Modifier.padding(top = 24.dp) + ) + + Text( + text = buildAnnotatedString { + append(stringResource(R.string.verify_display_fragment__pnp_verify_safety_numbers_explanation_with_s, displayName)) + append(" ") + withLink( + link = LinkAnnotation.Clickable( + tag = "verify-learn-more", + styles = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary + ) + ) + ) { + CommunicationActions.openBrowserLink(context, "https://signal.org/redirect/safety-numbers") + } + ) { + append(stringResource(R.string.LearnMoreTextView_learn_more)) + } + }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding( + start = 32.dp, + end = 32.dp, + top = 24.dp, + bottom = 20.dp + ) + ) + + AutomaticKeyVerificationBlock( + state = state, + emitter = emitter + ) + + Text( + text = buildAnnotatedString { + append(stringResource(R.string.verify_display_fragment__auto_verify_not_available)) + append(" ") + + val url = stringResource(R.string.verify_display_fragment__link) + withLink( + link = LinkAnnotation.Clickable( + tag = "auto-verify-learn-more", + styles = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary + ) + ) + ) { + CommunicationActions.openBrowserLink(context, url) + } + ) { + append(stringResource(R.string.LearnMoreTextView_learn_more)) + } + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 12.dp) + ) + } + } + + if (state.shouldDisplayVerifyAutomaticallyEducationSheet) { + ModalBottomSheet( + dragHandle = null, + onDismissRequest = { + emitter(VerifyDisplayScreenEvent.EducationDismiss) + } + ) { + VerifyEducationSheet( + onVerify = { + emitter(VerifyDisplayScreenEvent.EducationDismiss) + }, + onLearnMore = { + emitter(VerifyDisplayScreenEvent.EducationLearnMoreClick) + } + ) + } + } +} + +@Composable +private fun SafetyNumberQr( + state: VerifyDisplayScreenState, + onQrViewInitialized: (View) -> Unit, + emitter: (VerifyDisplayScreenEvent) -> Unit, + modifier: Modifier = Modifier +) { + val fingerprint = (state.fingerprintHolder as? FingerprintHolder.Initialised)?.fingerprint + var animateSuccess by remember { mutableStateOf(false) } + var animateFailure by remember { mutableStateOf(false) } + + AndroidView( + factory = { SafetyNumberQrView(it) }, + modifier = modifier + ) { + // TODO - qrCodeContainer has a context menu. + // TODO - animateVerifiedSuccess // animateVerifiedFailure + + if (fingerprint != null) { + it.setFingerprintViews(fingerprint.fingerprint, true) // TODO - animateCodeChanges + } + + if (animateSuccess) { + animateSuccess = false + it.animateVerifiedSuccess() + } + + if (animateFailure) { + animateFailure = false + it.animateVerifiedFailure() + } + + it.verifyButton.setOnClickListener { + emitter(VerifyDisplayScreenEvent.VerifyButtonClick(!state.isSafetyNumberVerified)) + } + + it.shareButton.setOnClickListener { + emitter(VerifyDisplayScreenEvent.ShareClick) + } + + it.qrCodeContainer.setOnClickListener { + emitter(VerifyDisplayScreenEvent.QrClick) + } + + onQrViewInitialized(it.numbersContainer) + + if (state.isSafetyNumberVerified) { + it.verifyButton.setText(R.string.verify_display_fragment__clear_verification) + } else { + it.verifyButton.setText(R.string.verify_display_fragment__mark_as_verified) + } + } + + if (state.fingerprintHolder == FingerprintHolder.NoFingerprintAvailable) { + YouMustFirstExchangeMessagesDialog(state, emitter) + } + + var alertMessage by remember { mutableStateOf(-1) } + + LaunchedEffect(state.clipComparisonResult?.submissionTime, state.scanComparisonResult?.submissionTime) { + when (state.clipComparisonResult) { + is VerifyDisplayScreenState.ClipComparisonResult.Failure -> animateFailure = true + is VerifyDisplayScreenState.ClipComparisonResult.NoDataInClipboard -> { + alertMessage = R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard + } + is VerifyDisplayScreenState.ClipComparisonResult.NoSafetyNumberInClipboard -> { + alertMessage = R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard + } + is VerifyDisplayScreenState.ClipComparisonResult.Success -> animateSuccess = true + null -> Unit + } + + when (state.scanComparisonResult) { + is VerifyDisplayScreenState.ScanComparisonResult.Failure -> animateFailure = true + is VerifyDisplayScreenState.ScanComparisonResult.IncorrectFormat -> { + alertMessage = R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number + } + is VerifyDisplayScreenState.ScanComparisonResult.Success -> animateSuccess = true + null -> Unit + } + } + + if (alertMessage > 0) { + Dialogs.SimpleMessageDialog( + message = stringResource(alertMessage), + dismiss = stringResource(android.R.string.ok), + onDismiss = { + alertMessage = -1 + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AutomaticKeyVerificationBlock( + state: VerifyDisplayScreenState, + emitter: (VerifyDisplayScreenEvent) -> Unit +) { + var displayEncryptedSheet by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .horizontalGutters() + ) { + Text( + text = stringResource(R.string.verify_display_fragment__automatic), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(vertical = 12.dp) + ) + + AnimatedContent( + targetState = state.automaticVerificationStatus, + modifier = Modifier + .fillMaxWidth() + .background(color = SignalTheme.colors.colorSurface1, shape = RoundedCornerShape(38.dp)) + .padding(horizontal = 16.dp) + ) { targetStatus -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable(enabled = targetStatus == AutomaticVerificationStatus.NONE, onClick = { + emitter(VerifyDisplayScreenEvent.VerifyAutomaticallyClick) + }, role = Role.Button) + ) { + if (targetStatus == AutomaticVerificationStatus.VERIFYING) { + CircularProgressIndicator( + modifier = Modifier + .padding(vertical = 16.dp) + .size(24.dp), + color = MaterialTheme.colorScheme.outline, + strokeCap = StrokeCap.Square, + strokeWidth = 2.dp + ) + } else { + StatusIcon(targetStatus) + } + + Text( + text = stringResource(getStatusText(targetStatus)), + modifier = Modifier + .padding(vertical = 16.dp) + .padding(start = 12.dp) + ) + + if (targetStatus != AutomaticVerificationStatus.NONE && targetStatus != AutomaticVerificationStatus.VERIFYING) { + IconButton(onClick = { + displayEncryptedSheet = true + }) { + Icon( + imageVector = SignalIcons.ChevronRight.imageVector, + contentDescription = null, + tint = MaterialTheme.colorScheme.outline, + modifier = Modifier + .padding(start = 2.dp) + .size(16.dp) + ) + } + } + } + } + } + + if (displayEncryptedSheet) { + state.recipient ?: return + val context = LocalContext.current + val name by rememberRecipientField(state.recipient) { getDisplayName(context) } + + ModalBottomSheet( + dragHandle = null, + onDismissRequest = { displayEncryptedSheet = false } + ) { + VerifiedSheet( + verifiedStatus = state.automaticVerificationStatus, + name = name, + onClick = { + displayEncryptedSheet = false + } + ) + } + } +} + +@Composable +private fun YouMustFirstExchangeMessagesDialog( + state: VerifyDisplayScreenState, + emitter: (VerifyDisplayScreenEvent) -> Unit +) { + state.recipient ?: return + + val context = LocalContext.current + val recipientName = rememberRecipientField(state.recipient) { getDisplayName(context) } + + Dialogs.SimpleMessageDialog( + message = stringResource(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, recipientName), + dismiss = stringResource(android.R.string.ok), + onDismiss = { + emitter(VerifyDisplayScreenEvent.YouMustFirstExchangeMessagesDialogDismiss) + } + ) +} + +@StringRes +private fun getStatusText(status: AutomaticVerificationStatus): Int { + return when (status) { + AutomaticVerificationStatus.NONE -> R.string.verify_display_fragment__verify_automatic + AutomaticVerificationStatus.VERIFYING -> R.string.verify_display_fragment__verifying + AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> R.string.verify_display_fragment__encryption_unavailable + AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY -> R.string.verify_display_fragment__encryption_unavailable + AutomaticVerificationStatus.VERIFIED -> R.string.verify_display_fragment__encryption_verified + } +} + +@Composable +private fun StatusIcon(status: AutomaticVerificationStatus) { + if (status == AutomaticVerificationStatus.VERIFYING) { + return + } + + val icon = when (status) { + AutomaticVerificationStatus.NONE -> ImageVector.vectorResource(R.drawable.symbol_key_24) + AutomaticVerificationStatus.VERIFYING -> error("None.") + AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> SignalIcons.Info.imageVector + AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY -> SignalIcons.Info.imageVector + AutomaticVerificationStatus.VERIFIED -> ImageVector.vectorResource(R.drawable.symbol_check_filled_circle_24) + } + + val tint = when (status) { + AutomaticVerificationStatus.NONE -> MaterialTheme.colorScheme.onSurface + AutomaticVerificationStatus.VERIFYING -> error("None.") + AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> MaterialTheme.colorScheme.onSurfaceVariant + AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY -> MaterialTheme.colorScheme.onSurfaceVariant + AutomaticVerificationStatus.VERIFIED -> Color(0xFF4CAF50) + } + + Icon( + imageVector = icon, + tint = tint, + contentDescription = null + ) +} + +@DayNightPreviews +@Composable +private fun VerifyDisplayScreenPreview() { + Previews.Preview { + var state by remember { + mutableStateOf( + VerifyDisplayScreenState( + isSafetyNumberVerified = false, + isAutomaticVerificationVisible = true, + shouldDisplayVerifyAutomaticallyEducationSheet = false, + recipient = Recipient( + isResolving = false, + systemContactName = "Miles Morales" + ) + ) + ) + } + + VerifyDisplayScreen( + state = state, + emitter = { + state = when (it) { + VerifyDisplayScreenEvent.VerifyAutomaticallyClick -> { + state.copy(automaticVerificationStatus = AutomaticVerificationStatus.VERIFYING) + } + + is VerifyDisplayScreenEvent.VerifyButtonClick -> { + state.copy(isSafetyNumberVerified = it.isVerified) + } + + else -> state + } + }, + onQrViewInitialized = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenEvent.kt new file mode 100644 index 0000000000..d7bc3b1e60 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenEvent.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.verify + +sealed interface VerifyDisplayScreenEvent { + data class VerifyButtonClick(val isVerified: Boolean) : VerifyDisplayScreenEvent + data object VerifyAutomaticallyClick : VerifyDisplayScreenEvent + data object ShareClick : VerifyDisplayScreenEvent + data object QrClick : VerifyDisplayScreenEvent + data object YouMustFirstExchangeMessagesDialogDismiss : VerifyDisplayScreenEvent + data object EducationDismiss : VerifyDisplayScreenEvent + data object EducationLearnMoreClick : VerifyDisplayScreenEvent +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenState.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenState.kt new file mode 100644 index 0000000000..1b709a7855 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenState.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.verify + +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.RemoteConfig + +data class VerifyDisplayScreenState( + val isSafetyNumberVerified: Boolean, + val isAutomaticVerificationVisible: Boolean = RemoteConfig.internalUser && SignalStore.settings.automaticVerificationEnabled, + val shouldDisplayVerifyAutomaticallyEducationSheet: Boolean = RemoteConfig.internalUser && SignalStore.settings.automaticVerificationEnabled && !SignalStore.uiHints.hasSeenVerifyAutomaticallySheet(), + val recipient: Recipient? = null, + val fingerprintHolder: FingerprintHolder = FingerprintHolder.Uninitialised, + val automaticVerificationStatus: AutomaticVerificationStatus = AutomaticVerificationStatus.NONE, + val clipComparisonResult: ClipComparisonResult? = null, + val scanComparisonResult: ScanComparisonResult? = null +) { + sealed interface ClipComparisonResult { + val submissionTime: Long + + data class NoDataInClipboard(override val submissionTime: Long = System.currentTimeMillis()) : ClipComparisonResult + data class NoSafetyNumberInClipboard(override val submissionTime: Long = System.currentTimeMillis()) : ClipComparisonResult + data class Success(override val submissionTime: Long = System.currentTimeMillis()) : ClipComparisonResult + data class Failure(override val submissionTime: Long = System.currentTimeMillis()) : ClipComparisonResult + } + + sealed interface ScanComparisonResult { + val submissionTime: Long + + data class IncorrectFormat(override val submissionTime: Long = System.currentTimeMillis()) : ScanComparisonResult + data class Success(override val submissionTime: Long = System.currentTimeMillis()) : ScanComparisonResult + data class Failure(override val submissionTime: Long = System.currentTimeMillis()) : ScanComparisonResult + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifySafetyNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenViewModel.kt similarity index 60% rename from app/src/main/java/org/thoughtcrime/securesms/verify/VerifySafetyNumberViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenViewModel.kt index aacfac66ba..f61dea37a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifySafetyNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayScreenViewModel.kt @@ -6,14 +6,20 @@ package org.thoughtcrime.securesms.verify import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import android.content.Intent +import android.text.TextUtils +import androidx.annotation.UiContext import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.asFlow import org.signal.core.util.concurrent.SignalDispatchers import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log @@ -32,25 +38,45 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.IdentityUtil +import java.nio.charset.StandardCharsets -class VerifySafetyNumberViewModel( +class VerifyDisplayScreenViewModel( + private val isSafetyNumberVerified: Boolean, private val recipientId: RecipientId, private val localIdentity: IdentityKey, private val remoteIdentity: IdentityKey ) : ViewModel() { companion object { - val TAG = Log.tag(VerifySafetyNumberViewModel::class.java) + val TAG = Log.tag(VerifyDisplayScreenViewModel::class.java) } - val recipient: LiveRecipient = Recipient.live(recipientId) + private val recipient: LiveRecipient = Recipient.live(recipientId) - private val fingerprintLiveData = MutableLiveData() - private val automaticVerificationLiveData = MutableLiveData(AutomaticVerificationStatus.NONE) + private val internalState = MutableStateFlow( + VerifyDisplayScreenState( + isSafetyNumberVerified = isSafetyNumberVerified + ) + ) + val state: StateFlow = internalState + + val fingerprintSnapshot: Fingerprint? + get() { + return when (val holder = state.value.fingerprintHolder) { + is FingerprintHolder.Initialised -> holder.fingerprint.fingerprint + else -> null + } + } init { initializeFingerprints() checkAutomaticVerificationEligibility() + + viewModelScope.launch { + recipient.observable().asFlow().collectLatest { r -> + internalState.update { it.copy(recipient = r) } + } + } } private fun checkAutomaticVerificationEligibility() { @@ -59,31 +85,85 @@ class VerifySafetyNumberViewModel( ProfileKeyUtil.profileKeyOrNull(recipient.get().profileKey) == null || SignalStore.misc.hasKeyTransparencyFailure ) { - automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_PERMANENT) + internalState.update { it.copy(automaticVerificationStatus = AutomaticVerificationStatus.UNAVAILABLE_PERMANENT) } + } + } + + fun setSeenEducationSheet() { + SignalStore.uiHints.setSeenVerifyAutomaticallySheet() + internalState.update { it.copy(shouldDisplayVerifyAutomaticallyEducationSheet = false) } + } + + fun setScannedFingerprint(scanned: String) { + fingerprintSnapshot?.let { fingerprint -> + try { + if (fingerprint.scannableFingerprint.compareTo(scanned.toByteArray(StandardCharsets.ISO_8859_1))) { + internalState.update { it.copy(scanComparisonResult = VerifyDisplayScreenState.ScanComparisonResult.Success()) } + } else { + internalState.update { it.copy(scanComparisonResult = VerifyDisplayScreenState.ScanComparisonResult.Failure()) } + } + } catch (e: Exception) { + Log.w(TAG, e) + internalState.update { it.copy(scanComparisonResult = VerifyDisplayScreenState.ScanComparisonResult.IncorrectFormat()) } + return + } + } + } + + @UiContext + fun createShareIntent(context: Context): Intent { + return VerifyDisplayRepository.createShareIntent(context, fingerprintSnapshot!!) + } + + fun copyFingerprintToClipboard() { + VerifyDisplayRepository.writeToClipboard(fingerprintSnapshot!!) + } + + fun compareClipboardToFingerprint() { + val clipboardData = VerifyDisplayRepository.readFromClipboard() + val fingerprint = (state.value.fingerprintHolder as? FingerprintHolder.Initialised)?.fingerprint + + if (clipboardData == null) { + internalState.update { it.copy(clipComparisonResult = VerifyDisplayScreenState.ClipComparisonResult.NoDataInClipboard()) } + return + } + + val numericClipboardData = clipboardData.replace("\\D".toRegex(), "") + if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length != 60) { + internalState.update { it.copy(clipComparisonResult = VerifyDisplayScreenState.ClipComparisonResult.NoSafetyNumberInClipboard()) } + return + } + + if (fingerprint?.fingerprint?.displayableFingerprint?.displayText == numericClipboardData) { + internalState.update { it.copy(clipComparisonResult = VerifyDisplayScreenState.ClipComparisonResult.Success()) } + } else { + internalState.update { it.copy(clipComparisonResult = VerifyDisplayScreenState.ClipComparisonResult.Failure()) } } } fun verifyAutomatically(canRetry: Boolean = true) { viewModelScope.launch(SignalDispatchers.IO) { - if (automaticVerificationLiveData.value == AutomaticVerificationStatus.UNAVAILABLE_PERMANENT || !isActive) { + if (internalState.value.automaticVerificationStatus == AutomaticVerificationStatus.UNAVAILABLE_PERMANENT || !isActive) { return@launch } - automaticVerificationLiveData.postValue(AutomaticVerificationStatus.VERIFYING) + internalState.update { it.copy(automaticVerificationStatus = AutomaticVerificationStatus.VERIFYING) } when (val result = VerifySafetyNumberRepository.verifyAutomatically(recipient.get())) { VerifySafetyNumberRepository.VerifyResult.Success -> { - automaticVerificationLiveData.postValue(AutomaticVerificationStatus.VERIFIED) + internalState.update { it.copy(automaticVerificationStatus = AutomaticVerificationStatus.VERIFIED) } } + is VerifySafetyNumberRepository.VerifyResult.RetryableFailure -> { if (canRetry) { delay(result.duration.toMillis()) verifyAutomatically(canRetry = false) } else { Log.i(TAG, "Got a retryable exception, but we already retried once. Ignoring.") - automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY) + internalState.update { it.copy(automaticVerificationStatus = AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY) } } } + VerifySafetyNumberRepository.VerifyResult.CorruptedFailure -> { Log.w(TAG, "KT store was corrupted. Clearing everything and starting again.") SignalStore.account.distinguishedHead = null @@ -92,11 +172,12 @@ class VerifySafetyNumberViewModel( verifyAutomatically(canRetry = false) } else { Log.i(TAG, "Store was corrupted and we can retry, but we already retried once. Ignoring.") - automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY) + internalState.update { it.copy(automaticVerificationStatus = AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY) } } } + VerifySafetyNumberRepository.VerifyResult.UnretryableFailure -> { - automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY) + internalState.update { it.copy(automaticVerificationStatus = AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY) } } } } @@ -117,19 +198,13 @@ class VerifySafetyNumberViewModel( aciFingerprint = SafetyNumberFingerprint(version, localIdentifier, localIdentity, remoteIdentifier, remoteIdentity, generator.createFor(version, localIdentifier, localIdentity, remoteIdentifier, remoteIdentity)) } - fingerprintLiveData.postValue(aciFingerprint) + internalState.update { state -> state.copy(fingerprintHolder = aciFingerprint?.let { FingerprintHolder.Initialised(it) } ?: FingerprintHolder.NoFingerprintAvailable) } } } - fun getFingerprint(): LiveData { - return fingerprintLiveData - } - - fun getAutomaticVerification(): LiveData { - return automaticVerificationLiveData - } - fun updateSafetyNumberVerification(verified: Boolean) { + internalState.update { it.copy(isSafetyNumberVerified = verified) } + val recipientId: RecipientId = recipientId val context: Context = AppDependencies.application @@ -171,12 +246,13 @@ class VerifySafetyNumberViewModel( } class Factory( + private val isSafetyNumberVerified: Boolean, private val recipientId: RecipientId, private val localIdentity: IdentityKey, private val remoteIdentity: IdentityKey ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return modelClass.cast(VerifySafetyNumberViewModel(recipientId, localIdentity, remoteIdentity))!! + return modelClass.cast(VerifyDisplayScreenViewModel(isSafetyNumberVerified, recipientId, localIdentity, remoteIdentity))!! } } } @@ -226,6 +302,12 @@ data class SafetyNumberFingerprint( } } +sealed interface FingerprintHolder { + data object Uninitialised : FingerprintHolder + data object NoFingerprintAvailable : FingerprintHolder + data class Initialised(val fingerprint: SafetyNumberFingerprint) : FingerprintHolder +} + enum class AutomaticVerificationStatus { NONE, VERIFYING, diff --git a/app/src/main/res/layout/verify_display_fragment.xml b/app/src/main/res/layout/verify_display_fragment.xml deleted file mode 100644 index a08881d1fe..0000000000 --- a/app/src/main/res/layout/verify_display_fragment.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file