summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-08-17 13:44:16 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-08-24 16:36:44 +0200
commit6b6b961f0de0014d3746f2ce7d775276eaa80e9b (patch)
treebb13691c19b896cd9528d706d4837619c42354c3 /android/app/src
parent3966954c5fbe487abd0863832afbeeaff54f19c0 (diff)
downloadmullvadvpn-6b6b961f0de0014d3746f2ce7d775276eaa80e9b.tar.xz
mullvadvpn-6b6b961f0de0014d3746f2ce7d775276eaa80e9b.zip
Migrate welcome view to compose
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt234
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AccountExpiryConstant.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt207
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt101
-rw-r--r--android/app/src/main/res/layout/welcome.xml66
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>