summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson90@gmail.com>2023-09-11 10:18:48 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-09-27 13:44:48 +0200
commit9bec28519867652cffc4e67be19f20ba53ebfab6 (patch)
tree5d12a3c27b8b04bce47e29646766f8eed193e239 /android
parentdab08e91532a65017c2acd7507d43901b9587406 (diff)
downloadmullvadvpn-9bec28519867652cffc4e67be19f20ba53ebfab6.tar.xz
mullvadvpn-9bec28519867652cffc4e67be19f20ba53ebfab6.zip
Add copy icon to WelcomeScreen
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CopyableObfuscationView.kt54
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt310
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt36
4 files changed, 256 insertions, 160 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CopyableObfuscationView.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CopyableObfuscationView.kt
index bf33c12ddd..ca3957ccb0 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CopyableObfuscationView.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CopyableObfuscationView.kt
@@ -4,8 +4,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+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.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
@@ -27,8 +29,7 @@ private fun PreviewCopyableObfuscationView() {
@Composable
fun CopyableObfuscationView(content: String) {
- val context = LocalContext.current
- val shouldObfuscated = remember { mutableStateOf(true) }
+ var shouldObfuscated by remember { mutableStateOf(true) }
Row(
verticalAlignment = CenterVertically,
@@ -36,7 +37,7 @@ fun CopyableObfuscationView(content: String) {
) {
AccountNumberView(
accountNumber = content,
- doObfuscateWithPasswordDots = shouldObfuscated.value,
+ doObfuscateWithPasswordDots = shouldObfuscated,
modifier = Modifier.weight(1f)
)
AnimatedIconButton(
@@ -44,25 +45,34 @@ fun CopyableObfuscationView(content: String) {
secondaryIcon = painterResource(id = R.drawable.icon_show),
isToggleButton = true,
contentDescription = stringResource(id = R.string.hide_account_number),
- onClick = { shouldObfuscated.value = shouldObfuscated.value.not() }
- )
- AnimatedIconButton(
- defaultIcon = painterResource(id = R.drawable.icon_copy),
- secondaryIcon = painterResource(id = R.drawable.icon_tick),
- secondaryIconColorFilter =
- ColorFilter.tint(color = MaterialTheme.colorScheme.inversePrimary),
- isToggleButton = false,
- contentDescription = stringResource(id = R.string.copy_account_number),
- onClick = {
- context.copyToClipboard(
- content = content,
- clipboardLabel = context.getString(R.string.mullvad_account_number)
- )
- SdkUtils.showCopyToastIfNeeded(
- context,
- context.getString(R.string.copied_mullvad_account_number)
- )
- }
+ onClick = { shouldObfuscated = !shouldObfuscated }
)
+
+ val context = LocalContext.current
+ val copy = {
+ context.copyToClipboard(
+ content = content,
+ clipboardLabel = context.getString(R.string.mullvad_account_number)
+ )
+ SdkUtils.showCopyToastIfNeeded(
+ context,
+ context.getString(R.string.copied_mullvad_account_number)
+ )
+ }
+
+ CopyAnimatedIconButton(onClick = copy)
}
}
+
+@Composable
+fun CopyAnimatedIconButton(onClick: () -> Unit) {
+ AnimatedIconButton(
+ defaultIcon = painterResource(id = R.drawable.icon_copy),
+ secondaryIcon = painterResource(id = R.drawable.icon_tick),
+ secondaryIconColorFilter =
+ ColorFilter.tint(color = MaterialTheme.colorScheme.inversePrimary),
+ isToggleButton = false,
+ contentDescription = stringResource(id = R.string.copy_account_number),
+ onClick = onClick
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
index 177cdc47e5..e0f84db5ea 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
@@ -5,6 +5,10 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.SnackbarData
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -34,6 +38,7 @@ fun ScaffoldWithTopBar(
onSettingsClicked: (() -> Unit)?,
onAccountClicked: (() -> Unit)?,
isIconAndLogoVisible: Boolean = true,
+ snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable (PaddingValues) -> Unit,
) {
val systemUiController = rememberSystemUiController()
@@ -52,11 +57,22 @@ fun ScaffoldWithTopBar(
isIconAndLogoVisible = isIconAndLogoVisible
)
},
+ snackbarHost = {
+ SnackbarHost(
+ snackbarHostState,
+ snackbar = { snackbarData -> MullvadSnackbar(snackbarData = snackbarData) }
+ )
+ },
content = content
)
}
@Composable
+fun MullvadSnackbar(snackbarData: SnackbarData) {
+ Snackbar(snackbarData = snackbarData, contentColor = MaterialTheme.colorScheme.secondary)
+}
+
+@Composable
@OptIn(ExperimentalToolbarApi::class)
fun CollapsableAwareToolbarScaffold(
backgroundColor: Color,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
index 8bcfc7ab49..b78f767f76 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
@@ -9,12 +9,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -35,11 +35,12 @@ import kotlinx.coroutines.flow.asSharedFlow
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.RedeemVoucherButton
import net.mullvad.mullvadvpn.compose.button.SitePaymentButton
+import net.mullvad.mullvadvpn.compose.component.CopyAnimatedIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.dialog.InfoDialog
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
+import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle
import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces
import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
import net.mullvad.mullvadvpn.lib.theme.AlphaTopBar
@@ -78,7 +79,7 @@ fun WelcomeScreen(
openConnectScreen: () -> Unit
) {
val context = LocalContext.current
- LaunchedEffect(key1 = Unit) {
+ LaunchedEffect(Unit) {
viewActions.collect { viewAction ->
when (viewAction) {
is WelcomeViewModel.ViewAction.OpenAccountView ->
@@ -88,6 +89,8 @@ fun WelcomeScreen(
}
}
val scrollState = rememberScrollState()
+ val snackbarHostState = remember { SnackbarHostState() }
+
ScaffoldWithTopBar(
topBarColor =
if (uiState.tunnelState.isSecured()) {
@@ -110,11 +113,10 @@ fun WelcomeScreen(
}
.copy(alpha = AlphaTopBar),
onSettingsClicked = onSettingsClick,
- onAccountClicked = onAccountClick
+ onAccountClicked = onAccountClick,
+ snackbarHostState = snackbarHostState
) {
Column(
- verticalArrangement = Arrangement.Bottom,
- horizontalAlignment = Alignment.Start,
modifier =
Modifier.fillMaxSize()
.verticalScroll(scrollState)
@@ -122,152 +124,184 @@ fun WelcomeScreen(
.background(color = MaterialTheme.colorScheme.primary)
.padding(it)
) {
- Text(
- text = stringResource(id = R.string.congrats),
- modifier =
- Modifier.padding(
+ // Welcome info area
+ WelcomeInfo(snackbarHostState, uiState, showSitePayment)
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ // Payment button area
+ PaymentPanel(showSitePayment, onSitePaymentClick, onRedeemVoucherClick)
+ }
+ }
+}
+
+@Composable
+private fun WelcomeInfo(
+ snackbarHostState: SnackbarHostState,
+ uiState: WelcomeUiState,
+ showSitePayment: Boolean
+) {
+ Column {
+ Text(
+ text = stringResource(id = R.string.congrats),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
top = Dimens.screenVerticalMargin,
start = Dimens.sideMargin,
end = Dimens.sideMargin
),
- style = MaterialTheme.typography.headlineLarge,
- color = MaterialTheme.colorScheme.onPrimary
- )
- Text(
- text = stringResource(id = R.string.here_is_your_account_number),
- modifier =
- Modifier.padding(
+ style = MaterialTheme.typography.headlineLarge,
+ color = MaterialTheme.colorScheme.onPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = stringResource(id = R.string.here_is_your_account_number),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
+ horizontal = Dimens.sideMargin,
vertical = Dimens.smallPadding,
- horizontal = Dimens.sideMargin
),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onPrimary
- )
- Text(
- text = uiState.accountNumber?.groupWithSpaces() ?: "",
- modifier =
- Modifier.fillMaxWidth()
- .wrapContentHeight()
- .then(
- uiState.accountNumber?.let {
- Modifier.clickable {
- context.copyToClipboard(
- content = uiState.accountNumber,
- clipboardLabel =
- context.getString(R.string.mullvad_account_number)
- )
- SdkUtils.showCopyToastIfNeeded(
- context,
- context.getString(R.string.copied_mullvad_account_number)
- )
- }
- }
- ?: Modifier
- )
- .padding(vertical = Dimens.smallPadding, horizontal = Dimens.sideMargin),
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onPrimary
- )
- Row(
- modifier = Modifier.padding(horizontal = Dimens.sideMargin),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- modifier = Modifier.weight(1f, fill = false),
- text =
- buildString {
- append(stringResource(id = R.string.device_name))
- append(": ")
- append(uiState.deviceName)
- },
- style = MaterialTheme.typography.bodySmall,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.onPrimary
- )
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
- var showDeviceNameDialog by remember { mutableStateOf(false) }
- IconButton(
- modifier = Modifier.align(Alignment.CenterVertically),
- onClick = { showDeviceNameDialog = true }
- ) {
- Icon(
- painter = painterResource(id = R.drawable.icon_info),
- contentDescription = null,
- tint = MullvadWhite
- )
- }
- if (showDeviceNameDialog) {
- InfoDialog(
- message =
- buildString {
- appendLine(
- stringResource(id = R.string.device_name_info_first_paragraph)
- )
- appendLine()
- appendLine(
- stringResource(id = R.string.device_name_info_second_paragraph)
- )
- appendLine()
- appendLine(
- stringResource(id = R.string.device_name_info_third_paragraph)
- )
- },
- onDismiss = { showDeviceNameDialog = false }
- )
- }
- }
- Text(
- text =
+ AccountNumberRow(snackbarHostState, uiState)
+
+ DeviceNameRow(deviceName = uiState.deviceName)
+
+ Text(
+ text =
+ buildString {
+ append(stringResource(id = R.string.pay_to_start_using))
+ if (showSitePayment) {
+ append(" ")
+ append(stringResource(id = R.string.add_time_to_account))
+ }
+ },
+ modifier =
+ Modifier.padding(
+ top = Dimens.smallPadding,
+ bottom = Dimens.verticalSpace,
+ start = Dimens.sideMargin,
+ end = Dimens.sideMargin
+ ),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+}
+
+@Composable
+private fun AccountNumberRow(snackbarHostState: SnackbarHostState, uiState: WelcomeUiState) {
+ val copiedAccountNumberMessage = stringResource(id = R.string.copied_mullvad_account_number)
+ val copyToClipboard = createCopyToClipboardHandle(snackbarHostState = snackbarHostState)
+ val onCopyToClipboard = {
+ copyToClipboard(uiState.accountNumber ?: "", copiedAccountNumberMessage)
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier =
+ Modifier.fillMaxWidth()
+ .clickable(onClick = onCopyToClipboard)
+ .padding(horizontal = Dimens.sideMargin)
+ ) {
+ Text(
+ text = uiState.accountNumber?.groupWithSpaces() ?: "",
+ modifier = Modifier.weight(1f).padding(vertical = Dimens.smallPadding),
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+
+ CopyAnimatedIconButton(onCopyToClipboard)
+ }
+}
+
+@Composable
+fun DeviceNameRow(deviceName: String?) {
+ Row(
+ modifier = Modifier.padding(horizontal = Dimens.sideMargin),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ modifier = Modifier.weight(1f, fill = false),
+ text =
+ buildString {
+ append(stringResource(id = R.string.device_name))
+ append(": ")
+ append(deviceName)
+ },
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+
+ var showDeviceNameDialog by remember { mutableStateOf(false) }
+ IconButton(
+ modifier = Modifier.align(Alignment.CenterVertically),
+ onClick = { showDeviceNameDialog = true }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.icon_info),
+ contentDescription = null,
+ tint = MullvadWhite
+ )
+ }
+ if (showDeviceNameDialog) {
+ InfoDialog(
+ message =
buildString {
- append(stringResource(id = R.string.pay_to_start_using))
- if (showSitePayment) {
- append(" ")
- append(stringResource(id = R.string.add_time_to_account))
- }
+ appendLine(stringResource(id = R.string.device_name_info_first_paragraph))
+ appendLine()
+ appendLine(stringResource(id = R.string.device_name_info_second_paragraph))
+ appendLine()
+ appendLine(stringResource(id = R.string.device_name_info_third_paragraph))
},
+ onDismiss = { showDeviceNameDialog = false }
+ )
+ }
+ }
+}
+
+@Composable
+private fun PaymentPanel(
+ showSitePayment: Boolean,
+ onSitePaymentClick: () -> Unit,
+ onRedeemVoucherClick: () -> Unit
+) {
+ Column(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = Dimens.mediumPadding)
+ .background(color = MaterialTheme.colorScheme.background)
+ ) {
+ Spacer(modifier = Modifier.padding(top = Dimens.screenVerticalMargin))
+ if (showSitePayment) {
+ SitePaymentButton(
+ onClick = onSitePaymentClick,
+ isEnabled = true,
modifier =
Modifier.padding(
- top = Dimens.smallPadding,
start = Dimens.sideMargin,
end = Dimens.sideMargin,
- bottom = Dimens.verticalSpace
- ),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onPrimary
- )
- Spacer(modifier = Modifier.weight(1f))
- // Payment button area
- Column(
- modifier =
- Modifier.fillMaxWidth()
- .padding(top = Dimens.mediumPadding)
- .background(color = MaterialTheme.colorScheme.background)
- ) {
- Spacer(modifier = Modifier.padding(top = Dimens.screenVerticalMargin))
- if (showSitePayment) {
- SitePaymentButton(
- onClick = onSitePaymentClick,
- isEnabled = true,
- modifier =
- Modifier.padding(
- start = Dimens.sideMargin,
- end = Dimens.sideMargin,
- bottom = Dimens.screenVerticalMargin
- )
+ bottom = Dimens.screenVerticalMargin
)
- }
- RedeemVoucherButton(
- onClick = onRedeemVoucherClick,
- isEnabled = true,
- modifier =
- Modifier.padding(
- start = Dimens.sideMargin,
- end = Dimens.sideMargin,
- bottom = Dimens.screenVerticalMargin
- )
- )
- }
+ )
}
+ RedeemVoucherButton(
+ onClick = onRedeemVoucherClick,
+ isEnabled = true,
+ modifier =
+ Modifier.padding(
+ start = Dimens.sideMargin,
+ end = Dimens.sideMargin,
+ bottom = Dimens.screenVerticalMargin
+ )
+ )
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt
new file mode 100644
index 0000000000..6c5e80d6ed
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt
@@ -0,0 +1,36 @@
+package net.mullvad.mullvadvpn.compose.util
+
+import android.os.Build
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.AnnotatedString
+import kotlinx.coroutines.launch
+
+typealias CopyToClipboardHandle = (content: String, toastMessage: String?) -> Unit
+
+@Composable
+fun createCopyToClipboardHandle(
+ snackbarHostState: SnackbarHostState,
+): CopyToClipboardHandle {
+ val scope = rememberCoroutineScope()
+ val clipboardManager: ClipboardManager = LocalClipboardManager.current
+
+ return { textToCopy: String, toastMessage: String? ->
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && toastMessage != null) {
+ scope.launch {
+ // Dismiss to prevent queueing up of snackbar data.
+ snackbarHostState.currentSnackbarData?.dismiss()
+ snackbarHostState.showSnackbar(
+ message = toastMessage,
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
+
+ clipboardManager.setText(AnnotatedString(textToCopy))
+ }
+}