diff options
| author | saber safavi <saber.safavi@codic.se> | 2023-07-25 12:46:17 +0200 |
|---|---|---|
| committer | saber safavi <saber.safavi@codic.se> | 2023-07-28 16:54:15 +0200 |
| commit | 44424f8528129369dbaa7eac6ea8d3ca34742f9c (patch) | |
| tree | 9a843a5a5fb064f922df10dfe6480e14c2f8f4f0 /android/app/src | |
| parent | 73359230e0b56881227a993fd9e0f15593d44435 (diff) | |
| download | mullvadvpn-44424f8528129369dbaa7eac6ea8d3ca34742f9c.tar.xz mullvadvpn-44424f8528129369dbaa7eac6ea8d3ca34742f9c.zip | |
Add account screen
Diffstat (limited to 'android/app/src')
| -rw-r--r-- | android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt | 199 | ||||
| -rw-r--r-- | android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt | 267 |
2 files changed, 222 insertions, 244 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt new file mode 100644 index 0000000000..1e8b677e32 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt @@ -0,0 +1,199 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import me.onebone.toolbar.ScrollStrategy +import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.ActionButton +import net.mullvad.mullvadvpn.compose.component.CollapsableAwareToolbarScaffold +import net.mullvad.mullvadvpn.compose.component.CollapsingTopBar +import net.mullvad.mullvadvpn.compose.component.CopyableObfuscationView +import net.mullvad.mullvadvpn.compose.component.InformationView +import net.mullvad.mullvadvpn.compose.component.MissingPolicy +import net.mullvad.mullvadvpn.compose.state.AccountUiState +import net.mullvad.mullvadvpn.compose.theme.Dimens +import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord +import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser +import net.mullvad.mullvadvpn.util.toExpiryDateString +import net.mullvad.mullvadvpn.viewmodel.AccountViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewAccountScreen() { + AccountScreen( + uiState = + AccountUiState( + deviceName = "Test Name", + accountNumber = "1234123412341234", + accountExpiry = null + ), + viewActions = MutableSharedFlow<AccountViewModel.ViewAction>().asSharedFlow(), + ) +} + +@ExperimentalMaterial3Api +@Composable +fun AccountScreen( + uiState: AccountUiState, + viewActions: SharedFlow<AccountViewModel.ViewAction>, + onRedeemVoucherClick: () -> Unit = {}, + onManageAccountClick: () -> Unit = {}, + onLogoutClick: () -> Unit = {}, + onBackClick: () -> Unit = {} +) { + val context = LocalContext.current + val state = rememberCollapsingToolbarScaffoldState() + val progress = state.toolbarState.progress + + CollapsableAwareToolbarScaffold( + backgroundColor = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxSize(), + state = state, + scrollStrategy = ScrollStrategy.ExitUntilCollapsed, + isEnabledWhenCollapsable = true, + toolbar = { + val scaffoldModifier = + Modifier.road( + whenCollapsed = Alignment.TopCenter, + whenExpanded = Alignment.BottomStart + ) + CollapsingTopBar( + backgroundColor = MaterialTheme.colorScheme.secondary, + onBackClicked = { onBackClick() }, + title = stringResource(id = R.string.settings_account), + progress = progress, + modifier = scaffoldModifier, + backTitle = String(), + shouldRotateBackButtonDown = true + ) + }, + ) { + LaunchedEffect(Unit) { + viewActions.collect { viewAction -> + if (viewAction is AccountViewModel.ViewAction.OpenAccountView) { + context.openAccountPageInBrowser(viewAction.token) + } + } + } + Column( + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.Start, + modifier = + Modifier.background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .wrapContentHeight() + .animateContentSize() + ) { + Text( + style = MaterialTheme.typography.labelMedium, + text = stringResource(id = R.string.device_name), + modifier = Modifier.padding(start = Dimens.sideMargin, end = Dimens.sideMargin) + ) + + InformationView( + content = uiState.deviceName.capitalizeFirstCharOfEachWord(), + whenMissing = MissingPolicy.SHOW_SPINNER + ) + + Text( + style = MaterialTheme.typography.labelMedium, + text = stringResource(id = R.string.account_number), + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + top = Dimens.smallPadding + ) + ) + + CopyableObfuscationView(content = uiState.accountNumber) + + Text( + style = MaterialTheme.typography.labelMedium, + text = stringResource(id = R.string.paid_until), + modifier = Modifier.padding(start = Dimens.sideMargin, end = Dimens.sideMargin) + ) + + InformationView( + content = uiState.accountExpiry?.toExpiryDateString() ?: "", + whenMissing = MissingPolicy.SHOW_SPINNER + ) + + Spacer(modifier = Modifier.weight(1.0f)) + + ActionButton( + text = stringResource(id = R.string.manage_account), + onClick = { onManageAccountClick() }, + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.screenVerticalMargin + ), + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.surface + ) + ) + + ActionButton( + text = stringResource(id = R.string.redeem_voucher), + onClick = onRedeemVoucherClick, + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.screenVerticalMargin + ), + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.surface + ) + ) + + ActionButton( + text = stringResource(id = R.string.log_out), + onClick = onLogoutClick, + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.screenVerticalMargin + ), + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.error + ) + ) + + Spacer(modifier = Modifier.height(Dimens.cellHeight)) + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt index bf6dc71b22..4349e8ae64 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt @@ -1,271 +1,50 @@ package net.mullvad.mullvadvpn.ui.fragment -import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import java.text.DateFormat -import kotlin.properties.Delegates.observable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.BuildConfig +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes -import net.mullvad.mullvadvpn.lib.common.util.JobTracker -import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord -import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser -import net.mullvad.mullvadvpn.model.TunnelState -import net.mullvad.mullvadvpn.repository.AccountRepository -import net.mullvad.mullvadvpn.repository.DeviceRepository -import net.mullvad.mullvadvpn.ui.CollapsibleTitleController -import net.mullvad.mullvadvpn.ui.GroupedPasswordTransformationMethod -import net.mullvad.mullvadvpn.ui.GroupedTransformationMethod +import net.mullvad.mullvadvpn.compose.screen.AccountScreen +import net.mullvad.mullvadvpn.compose.theme.AppTheme import net.mullvad.mullvadvpn.ui.NavigationBarPainter import net.mullvad.mullvadvpn.ui.StatusBarPainter -import net.mullvad.mullvadvpn.ui.extension.requireMainActivity -import net.mullvad.mullvadvpn.ui.paintNavigationBar -import net.mullvad.mullvadvpn.ui.paintStatusBar -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState -import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache -import net.mullvad.mullvadvpn.ui.widget.AccountManagementButton -import net.mullvad.mullvadvpn.ui.widget.Button -import net.mullvad.mullvadvpn.ui.widget.CopyableInformationView -import net.mullvad.mullvadvpn.ui.widget.InformationView -import net.mullvad.mullvadvpn.ui.widget.RedeemVoucherButton -import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS -import net.mullvad.mullvadvpn.util.addDebounceForUnknownState -import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier -import net.mullvad.talpid.tunnel.ErrorStateCause -import org.joda.time.DateTime -import org.koin.android.ext.android.inject +import net.mullvad.mullvadvpn.viewmodel.AccountViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel class AccountFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter { + private val vm by viewModel<AccountViewModel>() - // Injected dependencies - private val accountRepository: AccountRepository by inject() - private val deviceRepository: DeviceRepository by inject() - private val serviceConnectionManager: ServiceConnectionManager by inject() - - private val dateStyle = DateFormat.MEDIUM - private val timeStyle = DateFormat.SHORT - private val expiryFormatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle) - - private var oldAccountExpiry: DateTime? = null - - private var currentAccountExpiry: DateTime? = null - set(value) { - field = value - - synchronized(this) { - if (value != oldAccountExpiry) { - oldAccountExpiry = null - } - } - } - - private var hasConnectivity = true - set(value) { - field = value - accountManagementButton.isEnabled = value - } - - private var isOffline = true - set(value) { - field = value - redeemVoucherButton.setEnabled(!value) - } - - private var isAccountNumberShown by - observable(false) { _, _, doShow -> - accountNumberView.informationState = - if (doShow) { - InformationView.Masking.Show(GroupedTransformationMethod()) - } else { - InformationView.Masking.Hide(GroupedPasswordTransformationMethod()) - } - } - - private lateinit var accountExpiryView: InformationView - private lateinit var accountNumberView: CopyableInformationView - private lateinit var deviceNameView: InformationView - private lateinit var accountManagementButton: AccountManagementButton - private lateinit var redeemVoucherButton: RedeemVoucherButton - private lateinit var titleController: CollapsibleTitleController - - @Deprecated("Refactor code to instead rely on Lifecycle.") private val jobTracker = JobTracker() - - override fun onAttach(activity: Activity) { - super.onAttach(activity) - requireMainActivity().enterSecureScreen(this) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycleScope.launchUiSubscriptionsOnResume() - } - + @OptIn(ExperimentalMaterial3Api::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { - val view = inflater.inflate(R.layout.account, container, false) - - view.findViewById<View>(R.id.close).setOnClickListener { - requireMainActivity().onBackPressed() - } - - accountManagementButton = - view.findViewById<AccountManagementButton>(R.id.account_management).apply { - setOnClickAction("openAccountPageInBrowser", jobTracker) { - isEnabled = false - serviceConnectionManager.authTokenCache()?.fetchAuthToken()?.let { token -> - context.openAccountPageInBrowser(token) + ): View? { + return inflater.inflate(R.layout.fragment_compose, container, false).apply { + findViewById<ComposeView>(R.id.compose_view).setContent { + AppTheme { + val state = vm.uiState.collectAsState().value + AccountScreen( + uiState = state, + viewActions = vm.viewActions, + onRedeemVoucherClick = { openRedeemVoucherFragment() }, + onManageAccountClick = vm::onManageAccountClick, + onLogoutClick = vm::onLogoutClick + ) { + activity?.onBackPressed() } - isEnabled = true - checkForAddedTime() } } - accountManagementButton.isVisible = BuildTypes.RELEASE != BuildConfig.BUILD_TYPE - - redeemVoucherButton = - view.findViewById<RedeemVoucherButton>(R.id.redeem_voucher).apply { - prepare(parentFragmentManager, jobTracker) - } - - view.findViewById<Button>(R.id.logout).setOnClickAction("logout", jobTracker) { - accountRepository.logout() } - - accountNumberView = - view.findViewById<CopyableInformationView>(R.id.account_number).apply { - informationState = - InformationView.Masking.Hide(GroupedPasswordTransformationMethod()) - onToggleMaskingClicked = { isAccountNumberShown = isAccountNumberShown.not() } - } - - accountExpiryView = view.findViewById(R.id.account_expiry) - deviceNameView = view.findViewById(R.id.device_name) - titleController = CollapsibleTitleController(view) - - return view - } - - override fun onResume() { - super.onResume() - paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.darkBlue)) - } - - override fun onStop() { - jobTracker.cancelAllJobs() - super.onStop() } - override fun onDestroyView() { - titleController.onDestroy() - super.onDestroyView() - } - - override fun onDetach() { - requireMainActivity().leaveSecureScreen(this) - super.onDetach() - } - - private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - launchUpdateTextOnDeviceChanges() - launchUpdateTextOnExpiryChanges() - launchTunnelStateSubscription() - launchRefreshDeviceStateAfterAnimation() - launchPaintStatusBarAfterTransition() - } - } - - private fun CoroutineScope.launchUpdateTextOnDeviceChanges() { - launch { - deviceRepository.deviceState - .debounce { - it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS) - } - .collect { state -> - accountNumberView.information = state.token() - deviceNameView.information = state.deviceName()?.capitalizeFirstCharOfEachWord() - } - } - } - - private fun CoroutineScope.launchUpdateTextOnExpiryChanges() { - launch { - accountRepository.accountExpiryState - .map { state -> state.date() } - .collect { expiryDate -> - currentAccountExpiry = expiryDate - updateAccountExpiry(expiryDate) - } - } - } - - private fun CoroutineScope.launchTunnelStateSubscription() { - launch { - serviceConnectionManager.connectionState - .flatMapLatest { state -> - if (state is ServiceConnectionState.ConnectedReady) { - callbackFlowFromNotifier(state.container.connectionProxy.onUiStateChange) - } else { - emptyFlow() - } - } - .collect { uiState -> - hasConnectivity = - uiState is TunnelState.Connected || - uiState is TunnelState.Disconnected || - (uiState is TunnelState.Error && !uiState.errorState.isBlocking) - isOffline = - uiState is TunnelState.Error && - uiState.errorState.cause is ErrorStateCause.IsOffline - } - } - } - - private fun CoroutineScope.launchPaintStatusBarAfterTransition() = launch { - transitionFinishedFlow.collect { - paintStatusBar(ContextCompat.getColor(requireContext(), R.color.darkBlue)) - } - } - - private fun CoroutineScope.launchRefreshDeviceStateAfterAnimation() = launch { - transitionFinishedFlow.collect { deviceRepository.refreshDeviceState() } - } - - private fun checkForAddedTime() { - currentAccountExpiry?.let { expiry -> oldAccountExpiry = expiry } - } - - private fun updateAccountExpiry(accountExpiry: DateTime?) { - if (accountExpiry != null) { - accountExpiryView.information = expiryFormatter.format(accountExpiry.toDate()) - } else { - accountExpiryView.information = null - accountRepository.fetchAccountExpiry() - } - } - - private fun showRedeemVoucherDialog() { + private fun openRedeemVoucherFragment() { val transaction = parentFragmentManager.beginTransaction() - transaction.addToBackStack(null) - RedeemVoucherDialogFragment().show(transaction, null) } } |
