Skip to content
1 change: 0 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<string name="add_element_content_description">Add Element</string>
<string name="search_content_description">Search</string>
<string name="open_website_content_description">Open website</string>
<string name="copy_to_clipboard_content_description">Copy to clipboard</string>

<string name="add_new_element">Add new Element</string>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.davis.keygo.core.ui.clipboard

import android.content.ClipData
import android.content.ClipDescription
import android.os.Build
import android.os.PersistableBundle
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.toClipEntry

suspend fun Clipboard.setText(label: String, text: String, sensitive: Boolean = false) {
val clipData = ClipData.newPlainText(label, text).apply {
if (sensitive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
description.extras = PersistableBundle().apply {
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
}
}

setClipEntry(clipData.toClipEntry())
}
116 changes: 85 additions & 31 deletions core/ui/src/main/kotlin/de/davis/keygo/core/ui/components/KeyGoCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.semantics.onClick as onClickAction

@Immutable
data class KeyGoCardProperties(
Expand Down Expand Up @@ -68,40 +70,92 @@ fun KeyGoCard(
elevation = elevation,
border = border,
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
leadingItem?.let {
Box(modifier = Modifier.minimumInteractiveComponentSize()) {
leadingItem()
}
}
KeyGoCardContent(
title = title,
leadingItem = leadingItem,
trailingItem = trailingItem,
content = content
)
}
}
}

Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodySmall
) {
title()
}
@Composable
fun KeyGoCard(
onClick: () -> Unit,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
onClickLabel: String? = null,
properties: KeyGoCardProperties = KeyGoCardProperties.outlined(),
leadingItem: @Composable (() -> Unit)? = null,
trailingItem: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
// Semantics apply innermost first, so Card's own clickable has already written its click
// action with a null label by the time this runs. Setting an accessibility action merges field
// by field, so a null action here keeps that click, which is what carries the enabled state.
val labelled =
if (onClickLabel == null) modifier
else modifier.semantics { onClickAction(label = onClickLabel, action = null) }

CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodyLarge
) {
content()
}
}
with(properties) {
Card(
onClick = onClick,
modifier = labelled,
shape = shape,
colors = colors,
elevation = elevation,
border = border,
) {
KeyGoCardContent(
title = title,
leadingItem = leadingItem,
trailingItem = trailingItem,
content = content
)
}
}
}

trailingItem?.let {
Box(modifier = Modifier.minimumInteractiveComponentSize()) {
trailingItem()
}
}
@Composable
private fun KeyGoCardContent(
title: @Composable () -> Unit,
leadingItem: @Composable (() -> Unit)?,
trailingItem: @Composable (() -> Unit)?,
content: @Composable ColumnScope.() -> Unit,
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
leadingItem?.let {
Box(modifier = Modifier.minimumInteractiveComponentSize()) {
leadingItem()
}
}

Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodySmall
) {
title()
}

CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodyLarge
) {
content()
}
}

trailingItem?.let {
Box(modifier = Modifier.minimumInteractiveComponentSize()) {
trailingItem()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
package de.davis.keygo.core.ui.theme

import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily

val KeyGoTypography = Typography()
val KeyGoTypography = Typography()

private val Secret = TextStyle(
fontFamily = FontFamily.Monospace,
fontFeatureSettings = "tnum",
)

/**
* The ambient text style with secret typography applied: monospace so lookalike characters stay
* apart, tabular figures so digits keep their column while a value scrolls.
*
* Use it for anything the user reads character by character, such as passwords, card numbers, CVVs
* and TOTP codes.
*
* [FontFamily.Monospace] resolves to whatever font the device ships under that alias. That is
* usually Roboto Mono, but it varies by OEM and none of them guarantee a slashed zero. Bundling a
* font to make that deterministic is a change to [Secret] alone.
*/
val secretTextStyle: TextStyle
@Composable get() = LocalTextStyle.current.merge(Secret)
1 change: 0 additions & 1 deletion core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

<string name="match_not_found">No matches found</string>

<string name="copy_to_clipboard_content_description">Copy to clipboard</string>
<string name="show_password_content_description">Show password</string>
<string name="hide_password_content_description">Hide password</string>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
package de.davis.keygo.feature.autofill.presentation.activity

import android.content.ClipData
import android.content.ClipDescription
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.service.autofill.Dataset
import android.view.autofill.AutofillManager
import androidx.activity.compose.setContent
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.toClipEntry
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController
import de.davis.keygo.core.identity.presentation.rememberBiometricUnlockAdapter
import de.davis.keygo.core.identity.presentation.useAdapter
import de.davis.keygo.core.security.domain.model.BiometricPolicy
import de.davis.keygo.core.security.presentation.rememberBiometricCryptoController
import de.davis.keygo.core.ui.clipboard.setText
import de.davis.keygo.core.ui.theme.KeyGoTheme
import de.davis.keygo.core.util.onFailure
import de.davis.keygo.core.util.onSuccess
Expand All @@ -37,6 +34,7 @@ import de.davis.keygo.feature.autofill.presentation.model.Request
import de.davis.keygo.feature.autofill.presentation.model.RequestData
import de.davis.keygo.feature.item.create.presentation.password.GeneratePasswordModalBottomSheet
import org.koin.androidx.compose.koinViewModel
import de.davis.keygo.core.item.R as CoreItemR


/**
Expand Down Expand Up @@ -67,20 +65,18 @@ internal class AutofillActivity : FragmentActivity() {
val biometricUnlockAdapter = rememberBiometricUnlockAdapter()

val clipboard = LocalClipboard.current
val passwordLabel = stringResource(CoreItemR.string.password)

ObserveAsEvents(viewModel.events) { event ->
when (event) {
AutofillEvent.Abort -> cancel()
is AutofillEvent.Fill -> {
event.copyToClipboard?.let {
val clipData = ClipData.newPlainText(it, it).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
description.extras = PersistableBundle().apply {
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
}
}

clipboard.setClipEntry(clipData.toClipEntry())
clipboard.setText(
label = passwordLabel,
text = it,
sensitive = true,
)
}

finishWithResult(event.dataset)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package de.davis.keygo.feature.credit_card.presentation

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Contactless
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
Expand All @@ -17,7 +15,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -51,10 +48,8 @@ private fun ScanCardPrompt(
modifier: Modifier = Modifier,
) {
KeyGoCard(
modifier = modifier
.fillMaxWidth()
.clip(CardDefaults.elevatedShape)
.clickable(onClick = onClick),
onClick = onClick,
modifier = modifier.fillMaxWidth(),
properties = KeyGoCardProperties.elevated(),
leadingItem = {
Icon(
Expand Down
Loading
Loading