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 19, 2023
1 parent 34874c9 commit 38b04d3
Show file tree
Hide file tree
Showing 8 changed files with 527 additions and 162 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package net.mullvad.mullvadvpn.compose.dialog

import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
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.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),
viewActions = MutableSharedFlow<Unit>().asSharedFlow(),
onRedeem = {},
onDismiss = {}
)
}
}

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

LaunchedEffect(Unit) { viewActions.collect { onDismiss() } }

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))
if (uiState.message != null) {
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,35 @@
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),
viewActions = MutableSharedFlow<Unit>().asSharedFlow(),
onRedeem = {},
onDismiss = {}
)
}
}

@Composable
internal fun RedeemVoucherDialogScreen(
uiState: VoucherDialogUiState,
viewActions: SharedFlow<Unit>,
onRedeem: (voucherCode: String) -> Unit,
onDismiss: () -> Unit
) {
RedeemVoucherDialog(uiState, viewActions, onRedeem, onDismiss)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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(null, false, false)
is Verifying -> VoucherDialogUiState("loading", false, true)
is Success -> VoucherDialogUiState("success", false, false)
is Error -> VoucherDialogUiState("error", true, false)
}
}
}
Loading

0 comments on commit 38b04d3

Please sign in to comment.