diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-08-17 13:44:16 +0200 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-08-24 16:36:44 +0200 |
| commit | 6b6b961f0de0014d3746f2ce7d775276eaa80e9b (patch) | |
| tree | bb13691c19b896cd9528d706d4837619c42354c3 /android/app/src | |
| parent | 3966954c5fbe487abd0863832afbeeaff54f19c0 (diff) | |
| download | mullvadvpn-6b6b961f0de0014d3746f2ce7d775276eaa80e9b.tar.xz mullvadvpn-6b6b961f0de0014d3746f2ce7d775276eaa80e9b.zip | |
Migrate welcome view to compose
Diffstat (limited to 'android/app/src')
8 files changed, 377 insertions, 247 deletions
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 new file mode 100644 index 0000000000..5e19291649 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt @@ -0,0 +1,234 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +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.component.ScaffoldWithTopBar +import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.state.WelcomeUiState +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.lib.common.util.SdkUtils +import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces +import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser +import net.mullvad.mullvadvpn.ui.extension.copyToClipboard +import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel + +@Preview +@Composable +private fun PreviewWelcomeScreen() { + AppTheme { + WelcomeScreen( + showSitePayment = true, + uiState = WelcomeUiState(accountNumber = "4444555566667777"), + viewActions = MutableSharedFlow<WelcomeViewModel.ViewAction>().asSharedFlow(), + onSitePaymentClick = {}, + onRedeemVoucherClick = {}, + onSettingsClick = {}, + openConnectScreen = {} + ) + } +} + +@Composable +fun WelcomeScreen( + showSitePayment: Boolean, + uiState: WelcomeUiState, + viewActions: SharedFlow<WelcomeViewModel.ViewAction>, + onSitePaymentClick: () -> Unit, + onRedeemVoucherClick: () -> Unit, + onSettingsClick: () -> Unit, + openConnectScreen: () -> Unit +) { + val context = LocalContext.current + LaunchedEffect(key1 = Unit) { + viewActions.collect { viewAction -> + when (viewAction) { + is WelcomeViewModel.ViewAction.OpenAccountView -> + context.openAccountPageInBrowser(viewAction.token) + WelcomeViewModel.ViewAction.OpenConnectScreen -> openConnectScreen() + } + } + } + val scrollState = rememberScrollState() + ScaffoldWithTopBar( + topBarColor = + if (uiState.tunnelState.isSecured()) { + MaterialTheme.colorScheme.inversePrimary + } else { + MaterialTheme.colorScheme.error + }, + statusBarColor = + if (uiState.tunnelState.isSecured()) { + MaterialTheme.colorScheme.inversePrimary + } else { + MaterialTheme.colorScheme.error + }, + navigationBarColor = MaterialTheme.colorScheme.background, + onSettingsClicked = onSettingsClick + ) { + Column( + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.Start, + modifier = + Modifier.fillMaxSize() + .verticalScroll(scrollState) + .drawVerticalScrollbar(scrollState) + .background(color = MaterialTheme.colorScheme.primary) + .padding(it) + ) { + Text( + text = stringResource(id = R.string.congrats), + modifier = + Modifier.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( + 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 + ) + 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, + 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) { + ActionButton( + onClick = onSitePaymentClick, + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.screenVerticalMargin + ), + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.surface + ) + ) { + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = stringResource(id = R.string.buy_credit), + textAlign = TextAlign.Center, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.Center) + ) + Image( + painter = painterResource(id = R.drawable.icon_extlink), + contentDescription = null, + modifier = + Modifier.align(Alignment.CenterEnd) + .padding(horizontal = Dimens.smallPadding) + ) + } + } + } + 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 + ) + ) + } + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt new file mode 100644 index 0000000000..b8a12ce4ae --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.compose.state + +import net.mullvad.mullvadvpn.model.TunnelState + +data class WelcomeUiState( + val tunnelState: TunnelState = TunnelState.Disconnected, + val accountNumber: String? = null +) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AccountExpiryConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AccountExpiryConstant.kt new file mode 100644 index 0000000000..dff48b6228 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AccountExpiryConstant.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.constant + +const val ACCOUNT_EXPIRY_POLL_INTERVAL: Long = 15 /* s */ * 1000 /* ms */ diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 76060b8340..047ff2a26e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -32,6 +32,7 @@ import net.mullvad.mullvadvpn.viewmodel.SelectLocationViewModel import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel +import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel import org.apache.commons.validator.routines.InetAddressValidator import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext @@ -91,6 +92,7 @@ val uiModule = module { viewModel { SelectLocationViewModel(get()) } viewModel { SettingsViewModel(get(), get()) } viewModel { VpnSettingsViewModel(get(), get(), get(), get()) } + viewModel { WelcomeViewModel(get(), get(), get()) } } const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt index 954e9dcedf..9b5eb395ad 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes import net.mullvad.mullvadvpn.lib.common.util.JobTracker import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser @@ -137,7 +138,7 @@ class OutOfTimeFragment : BaseFragment() { private fun CoroutineScope.launchExpiryPolling() = launch { while (true) { accountRepository.fetchAccountExpiry() - delay(POLL_INTERVAL) + delay(ACCOUNT_EXPIRY_POLL_INTERVAL) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt index a995e4f5b4..5d0eb3b690 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt @@ -1,194 +1,51 @@ package net.mullvad.mullvadvpn.ui.fragment -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView -import android.widget.Toast -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.launch +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.screen.WelcomeScreen +import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes -import net.mullvad.mullvadvpn.lib.common.util.JobTracker -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.serviceconnection.ServiceConnectionManager -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState -import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache -import net.mullvad.mullvadvpn.ui.widget.HeaderBar -import net.mullvad.mullvadvpn.ui.widget.RedeemVoucherButton -import net.mullvad.mullvadvpn.ui.widget.SitePaymentButton -import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS -import net.mullvad.mullvadvpn.util.addDebounceForUnknownState -import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier -import org.joda.time.DateTime -import org.koin.android.ext.android.inject - -val POLL_INTERVAL: Long = 15 /* s */ * 1000 /* ms */ +import net.mullvad.mullvadvpn.ui.MainActivity +import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel class WelcomeFragment : BaseFragment() { - // Injected dependencies - private val accountRepository: AccountRepository by inject() - private val deviceRepository: DeviceRepository by inject() - private val serviceConnectionManager: ServiceConnectionManager by inject() - - private lateinit var accountLabel: TextView - private lateinit var headerBar: HeaderBar - private lateinit var sitePaymentButton: SitePaymentButton - - @Deprecated("Refactor code to instead rely on Lifecycle.") private val jobTracker = JobTracker() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycleScope.launchUiSubscriptionsOnResume() - } + private val vm by viewModel<WelcomeViewModel>() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - val view = inflater.inflate(R.layout.welcome, container, false) - - headerBar = - view.findViewById<HeaderBar>(R.id.header_bar).apply { - tunnelState = TunnelState.Disconnected - } - - accountLabel = - view.findViewById<TextView>(R.id.account_number).apply { - setOnClickListener { copyAccountTokenToClipboard() } - } - - view.findViewById<TextView>(R.id.pay_to_start_using).text = buildString { - append(requireActivity().getString(R.string.pay_to_start_using)) - if (BuildConfig.BUILD_TYPE != BuildTypes.RELEASE) { - append(" ") - append(requireActivity().getString(R.string.add_time_to_account)) - } - } - - sitePaymentButton = - view.findViewById<SitePaymentButton>(R.id.site_payment).apply { - newAccount = true - - setOnClickAction("openAccountPageInBrowser", jobTracker) { - setEnabled(false) - serviceConnectionManager.authTokenCache()?.fetchAuthToken()?.let { token -> - context.openAccountPageInBrowser(token) - } - setEnabled(true) - } - } - - sitePaymentButton.isVisible = BuildTypes.RELEASE != BuildConfig.BUILD_TYPE - - view.findViewById<RedeemVoucherButton>(R.id.redeem_voucher).apply { - prepare(parentFragmentManager, jobTracker) - } - - return view - } - - override fun onStop() { - jobTracker.cancelAllJobs() - super.onStop() - } - - private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - launchUpdateAccountNumberOnDeviceChanges() - launchAdvanceToConnectViewOnExpiryExtended() - launchExpiryPolling() - launchTunnelStateSubscription() - } - } - - private fun CoroutineScope.launchUpdateAccountNumberOnDeviceChanges() = launch { - deviceRepository.deviceState - .debounce { it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS) } - .collect { state -> updateAccountNumber(state.token()) } - } - - private fun CoroutineScope.launchAdvanceToConnectViewOnExpiryExtended() = launch { - accountRepository.accountExpiryState.collect { checkExpiry(it.date()) } - } - - private fun CoroutineScope.launchExpiryPolling() = launch { - while (true) { - accountRepository.fetchAccountExpiry() - delay(POLL_INTERVAL) - } - } - - private fun CoroutineScope.launchTunnelStateSubscription() = launch { - serviceConnectionManager.connectionState - .flatMapLatest { state -> - if (state is ServiceConnectionState.ConnectedReady) { - callbackFlowFromNotifier(state.container.connectionProxy.onStateChange) - } else { - emptyFlow() + return inflater.inflate(R.layout.fragment_compose, container, false).apply { + findViewById<ComposeView>(R.id.compose_view).setContent { + AppTheme { + val state = vm.uiState.collectAsState().value + WelcomeScreen( + showSitePayment = BuildTypes.RELEASE != BuildConfig.BUILD_TYPE, + uiState = state, + viewActions = vm.viewActions, + onSitePaymentClick = vm::onSitePaymentClick, + onRedeemVoucherClick = { openRedeemVoucherFragment() }, + onSettingsClick = { openSettingsView() }, + openConnectScreen = { advanceToConnectScreen() } + ) } } - .collect { state -> updateUiForTunnelState(state) } - } - - private fun updateUiForTunnelState(tunnelState: TunnelState) { - headerBar.tunnelState = tunnelState - sitePaymentButton.isEnabled = tunnelState is TunnelState.Disconnected - } - - private fun updateAccountNumber(rawAccountNumber: String?) { - val accountText = rawAccountNumber?.let { account -> addSpacesToAccountText(account) } - - accountLabel.text = accountText ?: "" - accountLabel.setEnabled(accountText != null && accountText.length > 0) - } - - private fun addSpacesToAccountText(account: String): String { - val length = account.length - - if (length == 0) { - return "" - } else { - val numParts = (length - 1) / 4 + 1 - - val parts = - Array(numParts) { index -> - val startIndex = index * 4 - val endIndex = minOf(startIndex + 4, length) - - account.substring(startIndex, endIndex) - } - - return parts.joinToString(" ") } } - private fun checkExpiry(maybeExpiry: DateTime?) { - maybeExpiry?.let { expiry -> - val tomorrow = DateTime.now().plusHours(20) - - if (expiry.isAfter(tomorrow)) { - advanceToConnectScreen() - } - } + private fun openRedeemVoucherFragment() { + val transaction = parentFragmentManager.beginTransaction() + transaction.addToBackStack(null) + RedeemVoucherDialogFragment().show(transaction, null) } private fun advanceToConnectScreen() { @@ -198,17 +55,7 @@ class WelcomeFragment : BaseFragment() { } } - private fun copyAccountTokenToClipboard() { - val accountToken = accountLabel.text - val clipboardLabel = resources.getString(R.string.mullvad_account_number) - val toastMessage = resources.getString(R.string.copied_mullvad_account_number) - - val context = requireActivity() - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText(clipboardLabel, accountToken) - - clipboard.setPrimaryClip(clipData) - - Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show() + private fun openSettingsView() { + (context as? MainActivity)?.openSettings() } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt new file mode 100644 index 0000000000..94f3ea7684 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt @@ -0,0 +1,101 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.state.WelcomeUiState +import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.repository.AccountRepository +import net.mullvad.mullvadvpn.repository.DeviceRepository +import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy +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.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS +import net.mullvad.mullvadvpn.util.addDebounceForUnknownState +import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier +import org.joda.time.DateTime + +@OptIn(FlowPreview::class) +class WelcomeViewModel( + private val accountRepository: AccountRepository, + private val deviceRepository: DeviceRepository, + private val serviceConnectionManager: ServiceConnectionManager +) : ViewModel() { + + private val _viewActions = MutableSharedFlow<ViewAction>(extraBufferCapacity = 1) + val viewActions = _viewActions.asSharedFlow() + + val uiState = + serviceConnectionManager.connectionState + .flatMapLatest { state -> + if (state is ServiceConnectionState.ConnectedReady) { + flowOf(state.container) + } else { + emptyFlow() + } + } + .flatMapLatest { serviceConnection -> + combine( + serviceConnection.connectionProxy.tunnelUiStateFlow(), + deviceRepository.deviceState.debounce { + it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS) + } + ) { tunnelState, deviceState -> + WelcomeUiState(tunnelState = tunnelState, accountNumber = deviceState.token()) + } + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), WelcomeUiState()) + + init { + viewModelScope.launch { + accountRepository.accountExpiryState.collectLatest { accountExpiry -> + accountExpiry.date()?.let { expiry -> + val tomorrow = DateTime.now().plusHours(20) + + if (expiry.isAfter(tomorrow)) { + _viewActions.tryEmit(ViewAction.OpenConnectScreen) + } + } + } + } + viewModelScope.launch { + while (true) { + accountRepository.fetchAccountExpiry() + delay(ACCOUNT_EXPIRY_POLL_INTERVAL) + } + } + } + + private fun ConnectionProxy.tunnelUiStateFlow(): Flow<TunnelState> = + callbackFlowFromNotifier(this.onUiStateChange) + + fun onSitePaymentClick() { + viewModelScope.launch { + _viewActions.tryEmit( + ViewAction.OpenAccountView( + serviceConnectionManager.authTokenCache()?.fetchAuthToken() ?: "" + ) + ) + } + } + + sealed interface ViewAction { + data class OpenAccountView(val token: String) : ViewAction + data object OpenConnectScreen : ViewAction + } +} diff --git a/android/app/src/main/res/layout/welcome.xml b/android/app/src/main/res/layout/welcome.xml deleted file mode 100644 index e1c887ab96..0000000000 --- a/android/app/src/main/res/layout/welcome.xml +++ /dev/null @@ -1,66 +0,0 @@ -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:mullvad="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent"> - <net.mullvad.mullvadvpn.ui.widget.HeaderBar android:id="@+id/header_bar" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> - <ScrollView android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_alignParentBottom="true" - android:layout_below="@id/header_bar" - android:fillViewport="true"> - <LinearLayout android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - <TextView android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/side_margin" - android:layout_marginTop="@dimen/screen_vertical_margin" - android:textColor="@color/white" - android:textSize="@dimen/text_huge" - android:textStyle="bold" - android:text="@string/congrats" /> - <TextView android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/side_margin" - android:layout_marginTop="8dp" - android:layout_marginBottom="11dp" - android:textColor="@color/white" - android:textSize="@dimen/text_small" - android:text="@string/here_is_your_account_number" /> - <TextView android:id="@+id/account_number" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/side_margin" - android:paddingVertical="11dp" - android:clickable="true" - android:focusable="true" - android:background="?android:attr/selectableItemBackground" - android:textColor="@color/white" - android:textSize="@dimen/text_big" - android:textStyle="bold" - android:text="" /> - <TextView android:id="@+id/pay_to_start_using" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/side_margin" - android:layout_marginTop="11dp" - android:layout_marginBottom="@dimen/vertical_space" - android:textColor="@color/white" - android:textSize="@dimen/text_small" /> - <Space android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" /> - <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:orientation="vertical" - android:paddingTop="@dimen/button_separation" - android:paddingBottom="@dimen/screen_vertical_margin" - android:background="@color/darkBlue"> - <include layout="@layout/payment_buttons" /> - </LinearLayout> - </LinearLayout> - </ScrollView> -</RelativeLayout> |
