Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate redeem voucher dialog into compose #5166

Merged
merged 6 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Line wrap the file at 100 chars. Th
- Add Social media to content blockers.
- Migrate Report Problem view to compose.
- Migrate View Logs view to compose.
- Migrate voucher dialog to compose.

#### Linux
- Don't block forwarding of traffic when the split tunnel mark (ct mark) is set.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package net.mullvad.mullvadvpn.compose.dialog

import android.content.res.Configuration
import androidx.compose.foundation.Image
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.ActionButton
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.compose.textfield.GroupedTextField
import net.mullvad.mullvadvpn.compose.util.vouchersVisualTransformation
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
import net.mullvad.mullvadvpn.lib.theme.AlphaDescription
import net.mullvad.mullvadvpn.lib.theme.AlphaDisabled
import net.mullvad.mullvadvpn.lib.theme.AlphaInactive
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import org.joda.time.DateTimeConstants

@Preview(device = Devices.TV_720p)
@Composable
private fun PreviewRedeemVoucherDialog() {
AppTheme {
RedeemVoucherDialog(
uiState = VoucherDialogUiState.INITIAL,
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}
}

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_3)
@Composable
private fun PreviewRedeemVoucherDialogVerifying() {
AppTheme {
RedeemVoucherDialog(
uiState = VoucherDialogUiState("", VoucherDialogState.Verifying),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}
}

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_3)
@Composable
private fun PreviewRedeemVoucherDialogError() {
AppTheme {
RedeemVoucherDialog(
uiState = VoucherDialogUiState("", VoucherDialogState.Error("An Error message")),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}
}

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_3)
@Composable
private fun PreviewRedeemVoucherDialogSuccess() {
AppTheme {
RedeemVoucherDialog(
uiState = VoucherDialogUiState("", VoucherDialogState.Success(3600)),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}
}

@Composable
fun RedeemVoucherDialog(
uiState: VoucherDialogUiState,
onVoucherInputChange: (String) -> Unit = {},
onRedeem: (voucherCode: String) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
title = {
if (uiState.voucherViewModelState !is VoucherDialogState.Success)
Text(
text = stringResource(id = R.string.enter_voucher_code),
style = MaterialTheme.typography.titleMedium
)
},
confirmButton = {
Column {
if (uiState.voucherViewModelState !is VoucherDialogState.Success) {
ActionButton(
text = stringResource(id = R.string.redeem),
onClick = { onRedeem(uiState.voucherInput) },
modifier = Modifier.padding(bottom = Dimens.mediumPadding),
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
disabledContentColor =
MaterialTheme.colorScheme.onSurface
.copy(alpha = AlphaInactive)
.compositeOver(MaterialTheme.colorScheme.surface),
disabledContainerColor =
MaterialTheme.colorScheme.surface
.copy(alpha = AlphaDisabled)
.compositeOver(MaterialTheme.colorScheme.surface)
),
isEnabled = uiState.voucherInput.length == VOUCHER_LENGTH
)
}
ActionButton(
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
),
text =
stringResource(
id =
if (uiState.voucherViewModelState is VoucherDialogState.Success)
R.string.changes_dialog_dismiss_button
else R.string.cancel
),
onClick = onDismiss
)
}
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (uiState.voucherViewModelState is VoucherDialogState.Success) {
val days: Int =
(uiState.voucherViewModelState.addedTime /
DateTimeConstants.SECONDS_PER_DAY)
.toInt()
val message =
stringResource(
R.string.added_to_your_account,
when (days) {
0 -> {
stringResource(R.string.less_than_one_day)
}
in 1..59 -> {
pluralStringResource(id = R.plurals.days, count = days, days)
}
else -> {
pluralStringResource(
id = R.plurals.months,
count = days / 30,
days / 30
)
}
}
)
RedeemSuccessBody(message = message)
} else {

EnterVoucherBody(
uiState = uiState,
onVoucherInputChange = onVoucherInputChange,
onRedeem = onRedeem
)
}
}
},
containerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.onBackground,
onDismissRequest = onDismiss
)
}

@Composable
private fun RedeemSuccessBody(message: String) {
Image(
painter = painterResource(R.drawable.icon_success),
contentDescription = null,
modifier = Modifier.fillMaxWidth().height(Dimens.buttonHeight)
)
Text(
text = stringResource(id = R.string.voucher_success_title),
modifier =
Modifier.padding(
start = Dimens.smallPadding,
top = Dimens.successIconVerticalPadding,
)
.fillMaxWidth(),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.titleMedium
)

Text(
text = message,
modifier =
Modifier.padding(start = Dimens.smallPadding, top = Dimens.cellTopPadding)
.fillMaxWidth(),
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaDescription),
style = MaterialTheme.typography.labelMedium
)
}

@Composable
private fun EnterVoucherBody(
uiState: VoucherDialogUiState,
onVoucherInputChange: (String) -> Unit = {},
onRedeem: (voucherCode: String) -> Unit
) {
val textFieldFocusRequester = FocusRequester()
Box(Modifier.wrapContentSize().clickable { textFieldFocusRequester.requestFocus() }) {
GroupedTextField(
value = uiState.voucherInput,
onSubmit = { input ->
if (uiState.voucherInput.length == VOUCHER_LENGTH) {
onRedeem(input)
}
},
onValueChanged = { input -> onVoucherInputChange(input.uppercase()) },
isValidValue = uiState.voucherInput.isNotEmpty(),
keyboardType = KeyboardType.Password,
placeholderText = stringResource(id = R.string.voucher_hint),
placeHolderColor =
MaterialTheme.colorScheme.onPrimary
.copy(alpha = AlphaDisabled)
.compositeOver(MaterialTheme.colorScheme.primary),
visualTransformation = vouchersVisualTransformation(),
maxCharLength = VOUCHER_LENGTH,
onFocusChange = {},
isDigitsOnlyAllowed = false,
isEnabled = true,
modifier = Modifier.focusRequester(textFieldFocusRequester),
validateRegex = "^[A-Za-z0-9]*$".toRegex()
)
}
Spacer(modifier = Modifier.height(Dimens.smallPadding))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.height(Dimens.listIconSize).fillMaxWidth()
) {
if (uiState.voucherViewModelState is VoucherDialogState.Verifying) {
CircularProgressIndicator(
modifier =
Modifier.height(Dimens.loadingSpinnerSizeMedium)
.width(Dimens.loadingSpinnerSizeMedium),
color = MaterialTheme.colorScheme.onSecondary
)
Text(
text = stringResource(id = R.string.verifying_voucher),
modifier = Modifier.padding(start = Dimens.smallPadding),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodySmall
)
} else if (uiState.voucherViewModelState is VoucherDialogState.Error) {
Text(
text = uiState.voucherViewModelState.errorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.mullvad.mullvadvpn.compose.screen

import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.compose.dialog.RedeemVoucherDialog
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.lib.theme.AppTheme

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_3)
@Composable
private fun PreviewRedeemVoucherDialogScreen() {
AppTheme {
RedeemVoucherDialogScreen(
uiState = VoucherDialogUiState.INITIAL,
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
)
}
}

@Composable
internal fun RedeemVoucherDialogScreen(
uiState: VoucherDialogUiState,
onVoucherInputChange: (String) -> Unit = {},
onRedeem: (voucherCode: String) -> Unit,
onDismiss: () -> Unit
) {
RedeemVoucherDialog(uiState, onVoucherInputChange, onRedeem, onDismiss)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.mullvad.mullvadvpn.compose.state

data class VoucherDialogUiState(
val voucherInput: String = "",
val voucherViewModelState: VoucherDialogState = VoucherDialogState.Default
) {
companion object {
val INITIAL = VoucherDialogUiState()
}
}

sealed interface VoucherDialogState {

data object Default : VoucherDialogState

data object Verifying : VoucherDialogState

data class Success(val addedTime: Long) : VoucherDialogState

data class Error(val errorMessage: String) : VoucherDialogState
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand All @@ -58,7 +59,8 @@ fun CustomTextField(
isValidValue: Boolean,
isDigitsOnlyAllowed: Boolean,
defaultTextColor: Color = Color.White,
textAlign: TextAlign = TextAlign.Start
textAlign: TextAlign = TextAlign.Start,
visualTransformation: VisualTransformation = VisualTransformation.None
) {
val fontSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp
val shape = RoundedCornerShape(4.dp)
Expand Down Expand Up @@ -122,6 +124,7 @@ fun CustomTextField(
Text(
text = placeholderText,
color = placeholderTextColor,
style = TextStyle(fontSize = fontSize, textAlign = textAlign),
fontSize = fontSize,
textAlign = textAlign,
modifier = Modifier.fillMaxWidth()
Expand All @@ -131,6 +134,7 @@ fun CustomTextField(
}
},
cursorBrush = SolidColor(MullvadBlue),
visualTransformation = visualTransformation,
modifier =
modifier
.background(backgroundColor)
Expand Down
Loading
Loading