mirror of
https://github.com/bitwarden/android.git
synced 2026-03-25 07:41:55 -05:00
Copy auth code on item click (#28)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* Wrapper class for using the clipboard.
|
||||
*/
|
||||
interface BitwardenClipboardManager {
|
||||
|
||||
/**
|
||||
* Places the given [text] into the device's clipboard. Setting the data to [isSensitive] will
|
||||
* obfuscate the displayed data on the default popup (true by default). A toast will be
|
||||
* displayed on devices that do not have a default popup (pre-API 32) and will not be displayed
|
||||
* on newer APIs. If a toast is displayed, it will be formatted as "[text] copied" or if a
|
||||
* [toastDescriptorOverride] is provided, it will be formatted as
|
||||
* "[toastDescriptorOverride] copied".
|
||||
*/
|
||||
fun setText(
|
||||
text: AnnotatedString,
|
||||
isSensitive: Boolean = true,
|
||||
toastDescriptorOverride: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* See [setText] for more details.
|
||||
*/
|
||||
fun setText(
|
||||
text: String,
|
||||
isSensitive: Boolean = true,
|
||||
toastDescriptorOverride: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* See [setText] for more details.
|
||||
*/
|
||||
fun setText(
|
||||
text: Text,
|
||||
isSensitive: Boolean = true,
|
||||
toastDescriptorOverride: String? = null,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.os.persistableBundleOf
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.x8bit.bitwarden.authenticator.R
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.toAnnotatedString
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.ClearClipboardWorker
|
||||
|
||||
/**
|
||||
* Default implementation of the [BitwardenClipboardManager] interface.
|
||||
*/
|
||||
class BitwardenClipboardManagerImpl(
|
||||
private val context: Context,
|
||||
) : BitwardenClipboardManager {
|
||||
private val clipboardManager: ClipboardManager = requireNotNull(context.getSystemService())
|
||||
|
||||
override fun setText(
|
||||
text: AnnotatedString,
|
||||
isSensitive: Boolean,
|
||||
toastDescriptorOverride: String?,
|
||||
) {
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData
|
||||
.newPlainText("", text)
|
||||
.apply {
|
||||
description.extras = persistableBundleOf(
|
||||
"android.content.extra.IS_SENSITIVE" to isSensitive,
|
||||
)
|
||||
},
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
val descriptor = toastDescriptorOverride ?: text
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.resources.getString(R.string.value_has_been_copied, descriptor),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
val clearClipboardRequest: OneTimeWorkRequest =
|
||||
OneTimeWorkRequest
|
||||
.Builder(ClearClipboardWorker::class.java)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
"ClearClipboard",
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
clearClipboardRequest,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setText(text: String, isSensitive: Boolean, toastDescriptorOverride: String?) {
|
||||
setText(text.toAnnotatedString(), isSensitive, toastDescriptorOverride)
|
||||
}
|
||||
|
||||
override fun setText(text: Text, isSensitive: Boolean, toastDescriptorOverride: String?) {
|
||||
setText(text.toString(context.resources), isSensitive, toastDescriptorOverride)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.clipboard
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
|
||||
/**
|
||||
* A worker to clear the clipboard manager.
|
||||
*/
|
||||
class ClearClipboardWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||
Worker(appContext, workerParams) {
|
||||
|
||||
private val clipboardManager =
|
||||
appContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
override fun doWork(): Result {
|
||||
clipboardManager.clearPrimaryClip()
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.x8bit.bitwarden.authenticator.data.platform.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.DispatcherManagerImpl
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.SdkClientManagerImpl
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
@@ -18,6 +22,12 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object PlatformManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBitwardenClipboardManager(
|
||||
@ApplicationContext context: Context,
|
||||
): BitwardenClipboardManager = BitwardenClipboardManagerImpl(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBitwardenDispatchers(): DispatcherManager = DispatcherManagerImpl()
|
||||
|
||||
@@ -169,20 +169,21 @@ fun ItemListingScreen(
|
||||
LazyColumn {
|
||||
items(currentState.itemList) {
|
||||
VaultVerificationCodeItem(
|
||||
authCode = it.authCode,
|
||||
issuer = it.issuer,
|
||||
periodSeconds = it.periodSeconds,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
alertThresholdSeconds = it.alertThresholdSeconds,
|
||||
startIcon = it.startIcon,
|
||||
onItemClick = {
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.ItemClick(it.authCode)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
startIcon = it.startIcon,
|
||||
issuer = it.issuer,
|
||||
supportingLabel = it.supportingLabel,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
periodSeconds = it.periodSeconds,
|
||||
alertThresholdSeconds = it.alertThresholdSeconds,
|
||||
authCode = it.authCode,
|
||||
onCopyClick = { /*TODO*/ },
|
||||
onItemClick = {
|
||||
onNavigateToEditItemScreen(it.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.Verifi
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toViewState
|
||||
@@ -35,6 +36,7 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class ItemListingViewModel @Inject constructor(
|
||||
private val authenticatorRepository: AuthenticatorRepository,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
) : BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
|
||||
initialState = ItemListingState(
|
||||
@@ -84,7 +86,7 @@ class ItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
is ItemListingAction.ItemClick -> {
|
||||
sendEvent(ItemListingEvent.NavigateToEditItem(action.id))
|
||||
handleItemClick(action)
|
||||
}
|
||||
|
||||
is ItemListingAction.DialogDismiss -> {
|
||||
@@ -97,6 +99,15 @@ class ItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleItemClick(action: ItemListingAction.ItemClick) {
|
||||
clipboardManager.setText(action.authCode)
|
||||
sendEvent(
|
||||
ItemListingEvent.ShowToast(
|
||||
message = R.string.value_has_been_copied.asText(action.authCode)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleInternalAction(internalAction: ItemListingAction.Internal) {
|
||||
when (internalAction) {
|
||||
is ItemListingAction.Internal.AuthCodesUpdated -> {
|
||||
@@ -479,7 +490,7 @@ sealed class ItemListingAction {
|
||||
/**
|
||||
* The user clicked a list item.
|
||||
*/
|
||||
data class ItemClick(val id: String) : ItemListingAction()
|
||||
data class ItemClick(val authCode: String) : ItemListingAction()
|
||||
|
||||
/**
|
||||
* The user dismissed the dialog.
|
||||
|
||||
@@ -9,16 +9,12 @@ import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -36,7 +32,6 @@ import com.x8bit.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
* @param periodSeconds The times span where the code is valid.
|
||||
* @param timeLeftSeconds The seconds remaining until a new code is needed.
|
||||
* @param startIcon The leading icon for the item.
|
||||
* @param onCopyClick The lambda function to be invoked when the copy button is clicked.
|
||||
* @param onItemClick The lambda function to be invoked when the item is clicked.
|
||||
* @param modifier The modifier for the item.
|
||||
* @param supportingLabel The supporting label for the item.
|
||||
@@ -50,7 +45,6 @@ fun VaultVerificationCodeItem(
|
||||
timeLeftSeconds: Int,
|
||||
alertThresholdSeconds: Int,
|
||||
startIcon: IconData,
|
||||
onCopyClick: () -> Unit,
|
||||
onItemClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
supportingLabel: String? = null,
|
||||
@@ -112,17 +106,6 @@ fun VaultVerificationCodeItem(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = onCopyClick,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_copy),
|
||||
contentDescription = stringResource(id = R.string.copy),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,16 +115,15 @@ fun VaultVerificationCodeItem(
|
||||
private fun VerificationCodeItem_preview() {
|
||||
AuthenticatorTheme {
|
||||
VaultVerificationCodeItem(
|
||||
startIcon = IconData.Local(R.drawable.ic_login_item),
|
||||
issuer = "Sample Label",
|
||||
supportingLabel = "Supporting Label",
|
||||
authCode = "1234567890".chunked(3).joinToString(" "),
|
||||
timeLeftSeconds = 15,
|
||||
issuer = "Sample Label",
|
||||
periodSeconds = 30,
|
||||
onCopyClick = {},
|
||||
timeLeftSeconds = 15,
|
||||
alertThresholdSeconds = 7,
|
||||
startIcon = IconData.Local(R.drawable.ic_login_item),
|
||||
onItemClick = {},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
alertThresholdSeconds = 7
|
||||
supportingLabel = "Supporting Label"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +86,5 @@
|
||||
<string name="unique_codes">Uniqe codes</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="tutorial">Tutorial</string>
|
||||
<string name="value_has_been_copied">%1$s copied</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user