Add UI for signal login view details screen.

This commit is contained in:
Greyson Parrelli
2026-08-21 17:36:10 -04:00
committed by Cody Henthorne
parent 5deaab3cf3
commit 91441f6fca
18 changed files with 709 additions and 161 deletions
+25
View File
@@ -0,0 +1,25 @@
plugins {
id("signal-library")
alias(libs.plugins.compose.compiler)
}
android {
namespace = "org.signal.signallogin"
buildFeatures {
compose = true
}
}
dependencies {
lintChecks(project(":lintchecks"))
api(project(":core:ui"))
implementation(project(":core:util-jvm"))
implementation(project(":core:models-jvm"))
implementation(libs.libsignal.android)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
}
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,17 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin
/**
* Test tags for the composables in this module, so UI tests can find them.
*/
object SignalLoginTestTags {
const val CARD_VIEW_DETAILS_BUTTON = "signal_login_card_view_details_button"
const val VIEW_DETAILS_SCREEN = "signal_login_view_details_screen"
const val VIEW_DETAILS_SAVE_TO_PASSWORD_MANAGER_BUTTON = "signal_login_view_details_save_to_password_manager_button"
const val VIEW_DETAILS_SAVE_AS_PDF_BUTTON = "signal_login_view_details_save_as_pdf_button"
}
@@ -0,0 +1,195 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin.card
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.ServiceId
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.signallogin.R
import org.signal.signallogin.SignalLoginTestTags
import org.signal.signallogin.fonts.MonoTypeface
import java.util.UUID
/** Aspect ratio of the credential card artwork, so it scales with the available width. */
private const val CARD_ASPECT_RATIO = 363f / 220f
private val CARD_MAX_WIDTH = 363.dp
/** Number of masking dots shown in front of the revealed suffix of each credential. */
private const val MASK_DOT_COUNT = 4
/** Number of trailing characters of each credential that are left visible. */
private const val VISIBLE_SUFFIX_LENGTH = 4
// Vertical space within the card is split by weight, using the gaps from the design (in its 220dp-tall coordinates) so
// that everything scales together with the artwork.
private const val WORDMARK_WEIGHT = 80f
private const val PILL_GAP_WEIGHT = 28f
private const val BOTTOM_WEIGHT = 24f
/**
* The Signal-branded card showing a Signal Login, masked down to the final few characters of each credential.
*
* @param aci The account key. Only the last few characters are shown.
* @param aep The recovery key. Only the last few characters are shown.
* @param onViewDetailsClicked Invoked when the user taps the "View details" pill on the card.
*/
@Composable
fun SignalLoginCard(
aci: ServiceId.ACI,
aep: AccountEntropyPool,
onViewDetailsClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.widthIn(max = CARD_MAX_WIDTH)
.fillMaxWidth()
.aspectRatio(CARD_ASPECT_RATIO)
) {
Image(
painter = painterResource(R.drawable.image_signal_login_card),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier.fillMaxSize()
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp)
) {
// The card artwork scales with the box, so the space it reserves for the baked-in "Signal" wordmark is
// apportioned by weight rather than a fixed height.
Spacer(modifier = Modifier.weight(WORDMARK_WEIGHT))
Row(modifier = Modifier.fillMaxWidth()) {
MaskedCredential(
label = stringResource(R.string.SignalLoginCard__account),
visibleSuffix = aci.toString().takeLast(VISIBLE_SUFFIX_LENGTH).uppercase(),
modifier = Modifier.weight(1f)
)
MaskedCredential(
label = stringResource(R.string.SignalLoginCard__recovery),
visibleSuffix = aep.displayValue.takeLast(VISIBLE_SUFFIX_LENGTH),
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.weight(PILL_GAP_WEIGHT))
ViewDetailsButton(
onClick = onViewDetailsClicked,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.weight(BOTTOM_WEIGHT))
}
}
}
@Composable
private fun MaskedCredential(
label: String,
visibleSuffix: String,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = Color.White.copy(alpha = 0.6f)
)
Row(verticalAlignment = Alignment.CenterVertically) {
repeat(MASK_DOT_COUNT) {
Box(
modifier = Modifier
.padding(end = 8.dp)
.size(7.dp)
.clip(CircleShape)
.background(Color.White)
)
}
Spacer(modifier = Modifier.width(4.dp))
Text(
text = visibleSuffix,
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = MonoTypeface.fontFamily(),
fontSize = 15.sp,
letterSpacing = 2.sp
),
color = Color.White
)
}
}
}
@Composable
private fun ViewDetailsButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(18.dp))
.background(Color.White.copy(alpha = 0.2f))
.clickable(onClick = onClick)
.padding(horizontal = 20.dp, vertical = 8.dp)
.testTag(SignalLoginTestTags.CARD_VIEW_DETAILS_BUTTON)
) {
Text(
text = stringResource(R.string.SignalLoginCard__view_details),
style = MaterialTheme.typography.labelLarge,
color = Color.White.copy(alpha = 0.96f)
)
}
}
@DayNightPreviews
@Composable
private fun SignalLoginCardPreview() {
Previews.Preview {
SignalLoginCard(
aci = ServiceId.ACI.from(UUID.fromString("a6b28482-2e32-83d0-7f23-91360a4c2b91")),
aep = AccountEntropyPool("uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t"),
onViewDetailsClicked = {}
)
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin.fonts
import android.graphics.Typeface
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
/**
* Special monospace font, primarily used for rendering AEPs.
*/
object MonoTypeface {
private var cached: Typeface? = null
@Composable
fun fontFamily(): FontFamily {
val context = LocalContext.current
return FontFamily(cached ?: Typeface.createFromAsset(context.assets, "fonts/MonoSpecial-Regular.otf").also { cached = it })
}
}
@@ -0,0 +1,269 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin.viewdetails
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
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.Texts
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.signallogin.R
import org.signal.signallogin.SignalLoginTestTags
import org.signal.signallogin.fonts.MonoTypeface
/** Size of the miniature credential card artwork shown at the top of the screen, from the design. */
private val MINI_CARD_WIDTH = 175.dp
private val MINI_CARD_HEIGHT = 100.dp
/** Corner radius of the card artwork (26dp in its 363dp-wide coordinates), scaled down to the miniature size. */
private val MINI_CARD_CORNER_RADIUS = 13.dp
private val BUTTON_MAX_WIDTH = 331.dp
private const val GROUPS_PER_ROW = 4
/** The least amount of space allowed between recovery key groups before falling back to natural text wrapping. */
private val MIN_GROUP_SPACING = 12.dp
/**
* Shows the user the full keys that make up their Signal Login and offers ways to save them.
*/
@Composable
fun SignalLoginViewDetailsScreen(
state: SignalLoginViewDetailsState,
onEvent: (SignalLoginViewDetailsScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
Scaffolds.Settings(
title = stringResource(R.string.SignalLoginViewDetailsScreen__signal_login),
onNavigationClick = { onEvent(SignalLoginViewDetailsScreenEvents.BackClicked) },
navigationIcon = SignalIcons.ArrowStart.imageVector,
navigationContentDescription = stringResource(R.string.SignalLoginViewDetailsScreen__navigate_back),
modifier = modifier.testTag(SignalLoginTestTags.VIEW_DETAILS_SCREEN)
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
MiniCard(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 20.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Texts.SectionHeader(text = stringResource(R.string.SignalLoginViewDetailsScreen__account_key))
KeyBlock(text = state.accountKey)
Texts.SectionHeader(text = stringResource(R.string.SignalLoginViewDetailsScreen__recovery_key))
RecoveryKeyBlock(groups = state.recoveryKeyGroups)
}
Footer(onEvent = onEvent)
}
}
}
/**
* A miniature of the credential card artwork, without any of the card's content.
*/
@Composable
private fun MiniCard(modifier: Modifier = Modifier) {
Image(
painter = painterResource(R.drawable.image_signal_login_card),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = modifier
.size(width = MINI_CARD_WIDTH, height = MINI_CARD_HEIGHT)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(MINI_CARD_CORNER_RADIUS))
)
}
/**
* A full credential rendered in the special monospace font on a rounded surface.
*/
@Composable
private fun KeyBlock(
text: String,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.keyBlockSurface()) {
Text(
text = text,
style = keyTextStyle()
)
}
}
/**
* The recovery key rendered as character groups. When four groups fit per row with at least
* [MIN_GROUP_SPACING] between them, renders rows of four groups evenly spaced across the full
* width. Otherwise renders the whole key as a single space-separated string that wraps naturally.
*/
@Composable
private fun RecoveryKeyBlock(
groups: List<String>,
modifier: Modifier = Modifier
) {
BoxWithConstraints(modifier = modifier.keyBlockSurface()) {
val style = keyTextStyle()
val textMeasurer = rememberTextMeasurer()
val maxWidth = constraints.maxWidth
val groupWidth = remember(groups, style) {
groups.maxOfOrNull { group -> textMeasurer.measure(text = group, style = style).size.width } ?: 0
}
val minSpacing = with(LocalDensity.current) { MIN_GROUP_SPACING.roundToPx() }
val fitsFourPerRow = groupWidth * GROUPS_PER_ROW + minSpacing * (GROUPS_PER_ROW - 1) <= maxWidth
if (fitsFourPerRow) {
val spacing = with(LocalDensity.current) { ((maxWidth - groupWidth * GROUPS_PER_ROW) / (GROUPS_PER_ROW - 1)).toDp() }
Column {
groups.chunked(GROUPS_PER_ROW).forEach { row ->
Row(
horizontalArrangement = Arrangement.spacedBy(spacing),
modifier = Modifier.fillMaxWidth()
) {
row.forEach { group ->
Text(
text = group,
style = style
)
}
}
}
}
} else {
Text(
text = groups.joinToString(separator = " "),
style = style
)
}
}
}
@Composable
private fun Modifier.keyBlockSurface(): Modifier {
return this
.horizontalGutters()
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(SignalTheme.colors.colorSurface2)
.padding(horizontal = 28.dp, vertical = 20.dp)
}
@Composable
private fun keyTextStyle(): TextStyle {
return MaterialTheme.typography.bodyLarge.copy(
fontFamily = MonoTypeface.fontFamily(),
fontSize = 18.sp,
lineHeight = 28.sp,
letterSpacing = 1.44.sp
)
}
@Composable
private fun Footer(onEvent: (SignalLoginViewDetailsScreenEvents) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp)
) {
Buttons.MediumTonal(
onClick = { onEvent(SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked) },
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
modifier = Modifier
.widthIn(max = BUTTON_MAX_WIDTH)
.fillMaxWidth()
.testTag(SignalLoginTestTags.VIEW_DETAILS_SAVE_TO_PASSWORD_MANAGER_BUTTON)
) {
Text(stringResource(R.string.SignalLoginViewDetailsScreen__save_to_password_manager))
}
Buttons.MediumTonal(
onClick = { onEvent(SignalLoginViewDetailsScreenEvents.SaveAsPdfClicked) },
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
modifier = Modifier
.widthIn(max = BUTTON_MAX_WIDTH)
.fillMaxWidth()
.testTag(SignalLoginTestTags.VIEW_DETAILS_SAVE_AS_PDF_BUTTON)
) {
Text(stringResource(R.string.SignalLoginViewDetailsScreen__save_as_pdf))
}
}
}
@DayNightPreviews
@Composable
private fun SignalLoginViewDetailsScreenPreview() {
Previews.Preview {
SignalLoginViewDetailsScreen(
state = SignalLoginViewDetailsState(
accountKey = "A6B28482-2E32-83D0-7F23-91360A4C2B91",
recoveryKey = "UY38JH2778HJJHJ8LK19GA61S672JSJ=89R=23S6A578=9BAP92J2YH5T326VV7T"
),
onEvent = {}
)
}
}
@@ -0,0 +1,17 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin.viewdetails
sealed class SignalLoginViewDetailsScreenEvents {
/** The user tapped the back arrow. */
data object BackClicked : SignalLoginViewDetailsScreenEvents()
/** The user chose to store the credentials with the system password manager. */
data object SaveToPasswordManagerClicked : SignalLoginViewDetailsScreenEvents()
/** The user chose to save the credentials as a PDF. */
data object SaveAsPdfClicked : SignalLoginViewDetailsScreenEvents()
}
@@ -0,0 +1,26 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin.viewdetails
import org.signal.core.util.censor
/**
* State for the screen that shows the user the full keys that make up their Signal Login.
*/
data class SignalLoginViewDetailsState(
val accountKey: String = "",
val recoveryKey: String = ""
) {
companion object {
private const val GROUP_SIZE = 4
}
/** The recovery key broken into character groups, in display order. */
val recoveryKeyGroups: List<String>
get() = recoveryKey.chunked(GROUP_SIZE)
override fun toString(): String = "SignalLoginViewDetailsState(accountKey=${accountKey.censor()}, recoveryKey=${recoveryKey.censor()})"
}
@@ -0,0 +1,79 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.signallogin.viewdetails
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.ServiceId
import org.signal.core.ui.compose.EventDrivenViewModel
import org.signal.core.util.logging.Log
/**
* View model for [SignalLoginViewDetailsScreen].
*
* The screen only renders the credentials it is given, so the state is fully derived from the constructor arguments.
* None of the actions the screen can produce are implemented yet -- every event is routed here and handled explicitly
* so that filling in the business logic is a matter of replacing the TODO branches.
*/
class SignalLoginViewDetailsViewModel(
aci: ServiceId.ACI,
aep: AccountEntropyPool
) : EventDrivenViewModel<SignalLoginViewDetailsScreenEvents>(TAG) {
companion object {
private val TAG = Log.tag(SignalLoginViewDetailsViewModel::class)
}
private val _state = MutableStateFlow(
SignalLoginViewDetailsState(
accountKey = aci.toString().uppercase(),
recoveryKey = aep.displayValue
)
)
val state: StateFlow<SignalLoginViewDetailsState> = _state.asStateFlow()
init {
_state
.onEach { Log.d(TAG, "[State] $it") }
.launchIn(viewModelScope)
}
override suspend fun processEvent(event: SignalLoginViewDetailsScreenEvents) {
when (event) {
is SignalLoginViewDetailsScreenEvents.BackClicked -> {
// TODO [phonenumberless] Navigate back once this screen is hooked into a flow.
Log.i(TAG, "Back clicked, but navigation isn't implemented yet.")
}
is SignalLoginViewDetailsScreenEvents.SaveToPasswordManagerClicked -> {
// TODO [phonenumberless] Store the credentials via the credential manager.
Log.i(TAG, "Save to password manager clicked, but the flow isn't implemented yet.")
}
is SignalLoginViewDetailsScreenEvents.SaveAsPdfClicked -> {
// TODO [phonenumberless] Render the credentials to a PDF and hand it to the user.
Log.i(TAG, "Save as PDF clicked, but the flow isn't implemented yet.")
}
}
}
class Factory(
private val aci: ServiceId.ACI,
private val aep: AccountEntropyPool
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SignalLoginViewDetailsViewModel(aci, aep) as T
}
}
}
@@ -0,0 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="363dp"
android:height="220dp"
android:viewportWidth="363"
android:viewportHeight="220"
tools:ignore="VectorRaster">
<path
android:pathData="M26,0h311a26,26 0 0,1 26,26v168a26,26 0 0,1 -26,26h-311a26,26 0 0,1 -26,-26v-168a26,26 0 0,1 26,-26Z"
android:fillColor="#343DBF"
/>
<group android:translateX="-8" android:translateY="-4">
<clip-path android:pathData="M34,4h311a26,26 0 0,1 26,26v168a26,26 0 0,1 -26,26h-311a26,26 0 0,1 -26,-26v-168a26,26 0 0,1 26,-26Z" />
<path
android:pathData="M72.5 79.5C24.5 134.3 -26.1667 126.667 -45.5 116L-82 240L401.5 226.5L429 -81C391 -55.5 341.5 10 266 19C217.221 24.8148 132.5 11 72.5 79.5Z"
android:fillColor="#3F47C1"
android:fillAlpha="0.5"
/>
<path
android:pathData="M89.5 129.5C41.5 184.3 -9.16666 176.667 -28.5 166L-65 290L418.5 276.5L446 -31C408 -5.5 358.5 60 283 69C234.221 74.8148 149.5 61 89.5 129.5Z"
android:fillColor="#8389E5"
android:fillAlpha="0.2"
/>
<path
android:pathData="M109.5 188.5C61.5 243.3 10.8333 235.667 -8.5 225L-45 319L438.5 305.5L466 28C428 53.5 378.5 119 303 128C254.221 133.815 169.5 120 109.5 188.5Z"
android:fillColor="#858CE4"
android:fillAlpha="0.24"
/>
<path
android:pathData="M38.7139 46.2578C35.7061 46.2578 33.6436 44.958 33.042 43.1533C32.9668 42.917 32.9131 42.6592 32.9131 42.4014C32.9131 41.7031 33.3428 41.252 33.998 41.252C34.5459 41.252 34.9004 41.499 35.1367 42.0791C35.6201 43.5293 37.0273 44.2061 38.8213 44.2061C40.7871 44.2061 42.1943 43.1963 42.1943 41.8105C42.1943 40.6074 41.3779 39.8555 39.3047 39.4043L37.6074 39.0498C34.5137 38.3945 33.1064 36.9551 33.1064 34.7207C33.1064 32.0674 35.4375 30.2412 38.7354 30.2412C41.4102 30.2412 43.5156 31.498 44.1172 33.5713C44.1709 33.7217 44.2031 33.9043 44.2031 34.1299C44.2031 34.7529 43.7627 35.1611 43.1396 35.1611C42.5596 35.1611 42.2051 34.8926 41.958 34.334C41.4316 32.9053 40.25 32.293 38.7031 32.293C36.8877 32.293 35.5449 33.1738 35.5449 34.5918C35.5449 35.7197 36.3506 36.4717 38.3594 36.9014L40.0459 37.2559C43.3008 37.9434 44.6328 39.2432 44.6328 41.499C44.6328 44.4209 42.334 46.2578 38.7139 46.2578ZM48.098 32.9912C47.3568 32.9912 46.7553 32.3896 46.7553 31.6592C46.7553 30.918 47.3568 30.3379 48.098 30.3379C48.85 30.3379 49.4516 30.918 49.4516 31.6592C49.4516 32.3896 48.85 32.9912 48.098 32.9912ZM48.098 46.2041C47.3998 46.2041 46.9379 45.7207 46.9379 44.9795V35.7305C46.9379 34.9785 47.3998 34.4951 48.098 34.4951C48.7963 34.4951 49.2582 34.9785 49.2582 35.7305V44.9795C49.2582 45.7207 48.7963 46.2041 48.098 46.2041ZM56.7195 50.2539C54.6678 50.2324 53.0457 49.4375 52.2723 48.2129C52.1219 47.9551 52.0574 47.7188 52.0574 47.4502C52.0574 46.9131 52.4549 46.5264 53.0457 46.5264C53.4002 46.5264 53.6258 46.6445 53.9373 46.9561C54.8611 47.9229 55.6561 48.3418 56.7625 48.3633C58.6531 48.3848 59.8025 47.3105 59.8025 45.7314V43.9375H59.7488C59.115 45.1621 57.783 45.9678 56.1717 45.9678C53.325 45.9678 51.4773 43.7441 51.4773 40.2529C51.4773 36.7188 53.3035 34.5166 56.2254 34.5166C57.826 34.5166 59.0721 35.3223 59.7596 36.6006H59.8025V35.6875C59.8025 34.9355 60.3074 34.4951 60.9734 34.4951C61.6395 34.4951 62.1336 34.9355 62.1336 35.6875V45.6885C62.1336 48.4814 60.0818 50.2861 56.7195 50.2539ZM56.7947 44.0771C58.6102 44.0771 59.8133 42.5947 59.8133 40.2744C59.8133 37.9541 58.6102 36.4287 56.7947 36.4287C55.0115 36.4287 53.8514 37.9111 53.8514 40.2637C53.8514 42.627 55.0115 44.0771 56.7947 44.0771ZM65.9318 46.2041C65.2443 46.2041 64.7717 45.7422 64.7717 44.9795V35.666C64.7717 34.957 65.2014 34.4951 65.8781 34.4951C66.5441 34.4951 67.0061 34.957 67.0061 35.6768V36.5684H67.0598C67.6721 35.2793 68.8537 34.5059 70.5939 34.5059C73.0861 34.5059 74.5148 36.0957 74.5148 38.6846V44.9795C74.5148 45.7422 74.0314 46.2041 73.3439 46.2041C72.6672 46.2041 72.1838 45.7422 72.1838 44.9795V39.1357C72.1838 37.4385 71.3889 36.5039 69.7775 36.5039C68.1447 36.5039 67.092 37.6641 67.092 39.415V44.9795C67.092 45.7422 66.6086 46.2041 65.9318 46.2041ZM80.4186 46.1934C78.2057 46.1934 76.648 44.8184 76.648 42.7881C76.648 40.8115 78.1734 39.5977 80.8482 39.4473L83.9527 39.2646V38.3945C83.9527 37.1377 83.1041 36.3857 81.6861 36.3857C80.5689 36.3857 79.8277 36.7832 79.1832 37.8145C78.9469 38.1582 78.6461 38.3086 78.2379 38.3086C77.6578 38.3086 77.2389 37.9219 77.2389 37.3418C77.2389 37.1055 77.3033 36.8477 77.443 36.5898C78.0338 35.3115 79.7418 34.4951 81.7721 34.4951C84.5113 34.4951 86.2623 35.9453 86.2623 38.2119V45.0332C86.2623 45.7637 85.8004 46.2041 85.1451 46.2041C84.5006 46.2041 84.0602 45.7852 84.0387 45.0977V44.1416H83.985C83.3297 45.3984 81.8902 46.1934 80.4186 46.1934ZM81.0523 44.3564C82.6744 44.3564 83.9527 43.2393 83.9527 41.7676V40.876L81.1598 41.0479C79.774 41.1445 78.9898 41.7568 78.9898 42.7236C78.9898 43.7119 79.817 44.3564 81.0523 44.3564ZM90.1143 46.2041C89.4375 46.2041 88.9541 45.7422 88.9541 44.9795V31.5195C88.9541 30.7568 89.4375 30.2949 90.1143 30.2949C90.791 30.2949 91.2744 30.7568 91.2744 31.5195V44.9795C91.2744 45.7422 90.791 46.2041 90.1143 46.2041Z"
android:fillColor="#FFFFFF"
/>
</group>
<path
android:pathData="M26,1h311a25,25 0 0,1 25,25v168a25,25 0 0,1 -25,25h-311a25,25 0 0,1 -25,-25v-168a25,25 0 0,1 25,-25Z"
android:strokeColor="#333BA8"
android:strokeWidth="2"
/>
</vector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Signal Login credential card -->
<!-- Label for the account portion of the Signal Login on the credential card. -->
<string name="SignalLoginCard__account">Account</string>
<!-- Label for the recovery portion of the Signal Login on the credential card. -->
<string name="SignalLoginCard__recovery">Recovery</string>
<!-- Button on the credential card that reveals the full Signal Login. -->
<string name="SignalLoginCard__view_details">View details</string>
<!-- Signal Login view details screen -->
<!-- Title of the screen that shows the user the full keys that make up their Signal Login. -->
<string name="SignalLoginViewDetailsScreen__signal_login">Signal Login</string>
<!-- Content description for the back arrow in the top app bar. -->
<string name="SignalLoginViewDetailsScreen__navigate_back">Navigate back</string>
<!-- Section header above the account key. -->
<string name="SignalLoginViewDetailsScreen__account_key">Account key</string>
<!-- Section header above the recovery key. -->
<string name="SignalLoginViewDetailsScreen__recovery_key">Recovery key</string>
<!-- Action button that stores the Signal Login in the device password manager. -->
<string name="SignalLoginViewDetailsScreen__save_to_password_manager">Save to password manager</string>
<!-- Action button that saves the Signal Login as a PDF. -->
<string name="SignalLoginViewDetailsScreen__save_as_pdf">Save as PDF</string>
</resources>