Skip to content

Commit

Permalink
Migrate redeem voucher dialog into compose
Browse files Browse the repository at this point in the history
  • Loading branch information
sabercodic committed Sep 21, 2023
1 parent 455de91 commit 183a2a9
Show file tree
Hide file tree
Showing 11 changed files with 561 additions and 162 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package net.mullvad.mullvadvpn.compose.dialog

import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.ActionButton
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.compose.textfield.GroupedTextField
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
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

@Preview(device = Devices.TV_720p)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_3)
@Composable
private fun PreviewRedeemVoucherDialog() {
AppTheme {
RedeemVoucherDialog(
uiState = VoucherDialogUiState(null),
onRedeem = {},
onDismiss = {}
)
}
}

@Composable
fun RedeemVoucherDialog(
uiState: VoucherDialogUiState,
onRedeem: (voucherCode: String) -> Unit,
onDismiss: () -> Unit
) {
val voucher = remember { mutableStateOf("") }

AlertDialog(
title = {
Text(
text = stringResource(id = R.string.enter_voucher_code),
style = MaterialTheme.typography.titleMedium
)
},
confirmButton = {
Column {
ActionButton(
text = stringResource(id = R.string.redeem),
onClick = { onRedeem(voucher.value) },
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
disabledContentColor =
MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaInactive),
disabledContainerColor =
MaterialTheme.colorScheme.surface.copy(alpha = AlphaDisabled)
),
isEnabled = voucher.value.length == VOUCHER_LENGTH
)
ActionButton(
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
),
text = stringResource(id = R.string.cancel),
modifier = Modifier.padding(top = Dimens.mediumPadding),
onClick = onDismiss
)
}
},
text = {
Column {
GroupedTextField(
value = voucher.value,
onSubmit = { input ->
if (input.isNotEmpty()) {
onRedeem(input)
}
},
onValueChanged = { input ->
voucher.value = input.uppercase().format().replace(" ", "").replace("-", "")
},
isValidValue = voucher.value.isNotEmpty(),
keyboardType = KeyboardType.Text,
placeholderText = stringResource(id = R.string.voucher_hint),
placeHolderColor =
MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaDisabled),
visualTransformation = { voucher -> formatOtherVouchers(voucher) },
maxCharLength = VOUCHER_LENGTH,
onFocusChange = {},
isDigitsOnlyAllowed = false,
isEnabled = true,
validateRegex = "^[A-Za-z0-9 -]*$".toRegex()
)
Spacer(modifier = Modifier.height(Dimens.smallPadding))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.height(Dimens.listIconSize)
) {
if (uiState.showLoading) {
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 {

Text(
text = uiState.message ?: "",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
}
},
containerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.onBackground,
onDismissRequest = onDismiss
)
}

private fun formatOtherVouchers(text: AnnotatedString): TransformedText {
val trimmed = if (text.text.length >= 16) text.text.substring(0..15) else text.text
var out = ""

for (i in trimmed.indices) {
out += trimmed[i]
if (i % 4 == 3 && i != 15) out += "-"
}
val voucherOffsetTranslator =
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 3) return offset
if (offset <= 7) return offset + 1
if (offset <= 11) return offset + 2
if (offset <= 16) return offset + 3
return 19
}

override fun transformedToOriginal(offset: Int): Int {
if (offset <= 4) return offset
if (offset <= 9) return offset - 1
if (offset <= 14) return offset - 2
if (offset <= 19) return offset - 3
return 16
}
}

return TransformedText(AnnotatedString(out), voucherOffsetTranslator)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
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(null),
onRedeem = {},
onDismiss = {}
)
}
}

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

data class VoucherDialogUiState(
var message: String? = null,
var isError: Boolean = false,
var showLoading: Boolean = false
)

sealed class VoucherDialogViewModelState {
data object Default : VoucherDialogViewModelState()

data object Verifying : VoucherDialogViewModelState()

data class Success(var message: String?) : VoucherDialogViewModelState()

data class Error(val errorMessage: String) : VoucherDialogViewModelState()

fun toUiState(): VoucherDialogUiState {
return when (this) {
is Default -> VoucherDialogUiState(message = null, isError = false, showLoading = false)
is Verifying ->
VoucherDialogUiState(message = null, isError = false, showLoading = true)
is Success ->
VoucherDialogUiState(message = message, isError = false, showLoading = false)
is Error ->
VoucherDialogUiState(message = errorMessage, isError = true, showLoading = false)
}
}
}
Loading

0 comments on commit 183a2a9

Please sign in to comment.