From f74befefcd222f1f70be661c91f877d91a55cdb2 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Wed, 16 Oct 2024 07:56:35 -0400 Subject: [PATCH] android: add account integration (#1314) * android: add account integration * bump deps * remove query packages --- .../nymtech/nymvpn/data/SettingsRepository.kt | 5 - .../datastore/DataStoreSettingsRepository.kt | 14 --- .../nymtech/nymvpn/data/domain/Settings.kt | 2 - .../service/android/tile/VpnQuickTile.kt | 4 +- .../nymvpn/service/tunnel/NymTunnelManager.kt | 53 ++++++--- .../nymvpn/service/tunnel/TunnelManager.kt | 5 +- .../nymvpn/service/tunnel/TunnelState.kt | 1 + .../java/net/nymtech/nymvpn/ui/AppUiState.kt | 1 + .../net/nymtech/nymvpn/ui/AppViewModel.kt | 7 +- .../net/nymtech/nymvpn/ui/MainActivity.kt | 1 - .../nymvpn/ui/common/buttons/ScaledSwitch.kt | 9 +- .../ui/screens/analytics/AnalyticsScreen.kt | 4 - .../nymvpn/ui/screens/main/MainScreen.kt | 3 +- .../ui/screens/permission/PermissionScreen.kt | 14 ++- .../ui/screens/scanner/ScannerScreen.kt | 2 +- .../ui/screens/scanner/ScannerViewModel.kt | 22 ++-- .../ui/screens/settings/SettingsScreen.kt | 105 ++++++++---------- .../screens/settings/account/AccountScreen.kt | 64 +++++------ .../appearance/language/LanguageScreen.kt | 5 +- .../settings/credential/CredentialScreen.kt | 100 ++++++++++------- .../credential/CredentialViewModel.kt | 30 ++--- .../java/net/nymtech/nymvpn/ui/theme/Size.kt | 3 +- .../app/src/main/res/values/strings.xml | 14 +++ nym-vpn-android/gradle/libs.versions.toml | 11 +- .../src/main/AndroidManifest.xml | 1 - .../java/net/nymtech/vpn/backend/Backend.kt | 8 +- .../net/nymtech/vpn/backend/NymBackend.kt | 20 ++-- .../crates/nym-vpn-lib/src/platform/mod.rs | 2 + 28 files changed, 261 insertions(+), 249 deletions(-) diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/SettingsRepository.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/SettingsRepository.kt index 0bfac728b9..605075d9ef 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/SettingsRepository.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/SettingsRepository.kt @@ -5,7 +5,6 @@ import net.nymtech.nymvpn.data.domain.Settings import net.nymtech.nymvpn.ui.theme.Theme import net.nymtech.vpn.backend.Tunnel import net.nymtech.vpn.model.Country -import java.time.Instant interface SettingsRepository { @@ -50,10 +49,6 @@ interface SettingsRepository { suspend fun setApplicationShortcuts(enabled: Boolean) - suspend fun getCredentialExpiry(): Instant? - - suspend fun saveCredentialExpiry(instant: Instant) - suspend fun getEnvironment(): Tunnel.Environment suspend fun setEnvironment(environment: Tunnel.Environment) diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt index 5de785b5cb..0a0475c396 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt @@ -1,7 +1,6 @@ package net.nymtech.nymvpn.data.datastore import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -11,7 +10,6 @@ import net.nymtech.nymvpn.ui.theme.Theme import net.nymtech.vpn.backend.Tunnel import net.nymtech.vpn.model.Country import timber.log.Timber -import java.time.Instant class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager) : SettingsRepository { @@ -27,7 +25,6 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager private val autoStart = booleanPreferencesKey("AUTO_START") private val analyticsShown = booleanPreferencesKey("ANALYTICS_SHOWN") private val applicationShortcuts = booleanPreferencesKey("APPLICATION_SHORTCUTS") - private val credentialExpiry = longPreferencesKey("CREDENTIAL_EXPIRY") private val environment = stringPreferencesKey("ENVIRONMENT") override suspend fun init() { @@ -135,16 +132,6 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager dataStoreManager.saveToDataStore(applicationShortcuts, enabled) } - override suspend fun getCredentialExpiry(): Instant? { - return dataStoreManager.getFromStore(credentialExpiry)?.let { - Instant.ofEpochSecond(it) - } - } - - override suspend fun saveCredentialExpiry(instant: Instant) { - dataStoreManager.saveToDataStore(credentialExpiry, instant.epochSecond) - } - override suspend fun getEnvironment(): Tunnel.Environment { return dataStoreManager.getFromStore(environment)?.let { Tunnel.Environment.valueOf(it) @@ -181,7 +168,6 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager firstHopCountry = Country.from(pref[firstHopCountry]) ?: default, lastHopCountry = Country.from(pref[lastHopCountry]) ?: default, isShortcutsEnabled = pref[applicationShortcuts] ?: Settings.SHORTCUTS_DEFAULT, - credentialExpiry = pref[credentialExpiry]?.let { Instant.ofEpochSecond(it) }, environment = pref[environment]?.let { Tunnel.Environment.valueOf(it) } ?: Settings.DEFAULT_ENVIRONMENT, ) } catch (e: IllegalArgumentException) { diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/domain/Settings.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/domain/Settings.kt index 231d3c4f48..fdf6d4f17e 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/domain/Settings.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/data/domain/Settings.kt @@ -3,7 +3,6 @@ package net.nymtech.nymvpn.data.domain import net.nymtech.nymvpn.ui.theme.Theme import net.nymtech.vpn.backend.Tunnel import net.nymtech.vpn.model.Country -import java.time.Instant data class Settings( val theme: Theme? = null, @@ -16,7 +15,6 @@ data class Settings( val firstHopCountry: Country = Country(), val lastHopCountry: Country = Country(), val isShortcutsEnabled: Boolean = SHORTCUTS_DEFAULT, - val credentialExpiry: Instant? = null, val environment: Tunnel.Environment = DEFAULT_ENVIRONMENT, ) { companion object { diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/android/tile/VpnQuickTile.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/android/tile/VpnQuickTile.kt index f657ec023f..6ec4e34b99 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/android/tile/VpnQuickTile.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/android/tile/VpnQuickTile.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.launch import net.nymtech.nymvpn.R import net.nymtech.nymvpn.data.SettingsRepository import net.nymtech.nymvpn.service.tunnel.TunnelManager -import net.nymtech.nymvpn.util.extensions.isInvalid import net.nymtech.nymvpn.util.extensions.startTunnelFromBackground import net.nymtech.nymvpn.util.extensions.stopTunnelFromBackground import net.nymtech.vpn.backend.Tunnel @@ -40,8 +39,7 @@ class VpnQuickTile : TileService(), LifecycleOwner { lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleScope.launch { - val credExpiry = settingsRepository.getCredentialExpiry() - if (credExpiry.isInvalid()) return@launch setUnavailable() + if (!tunnelManager.isMnemonicStored()) return@launch setUnavailable() val state = tunnelManager.getState() kotlin.runCatching { when (state) { diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/NymTunnelManager.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/NymTunnelManager.kt index 4fe3a52c86..748a1711d5 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/NymTunnelManager.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/NymTunnelManager.kt @@ -2,15 +2,19 @@ package net.nymtech.nymvpn.service.tunnel import android.content.Context import android.net.VpnService +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import net.nymtech.nymvpn.NymVpn import net.nymtech.nymvpn.R import net.nymtech.nymvpn.data.SettingsRepository +import net.nymtech.nymvpn.module.qualifiers.ApplicationScope import net.nymtech.nymvpn.service.notification.NotificationService -import net.nymtech.nymvpn.util.extensions.isInvalid +import net.nymtech.nymvpn.util.Constants import net.nymtech.nymvpn.util.extensions.requestTileServiceStateUpdate import net.nymtech.nymvpn.util.extensions.toUserMessage import net.nymtech.vpn.backend.Backend @@ -20,7 +24,6 @@ import net.nymtech.vpn.model.Statistics import nym_vpn_lib.BandwidthStatus import nym_vpn_lib.VpnException import timber.log.Timber -import java.time.Instant import javax.inject.Inject import javax.inject.Provider @@ -29,10 +32,15 @@ class NymTunnelManager @Inject constructor( private val notificationService: NotificationService, private val backend: Provider, private val context: Context, + @ApplicationScope private val applicationScope: CoroutineScope, ) : TunnelManager { private val _state = MutableStateFlow(TunnelState()) - override val stateFlow: Flow = _state.asStateFlow() + override val stateFlow: Flow = _state.onStart { + _state.update { + it.copy(isMnemonicStored = isMnemonicStored()) + } + }.stateIn(applicationScope, SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), TunnelState()) @get:Synchronized @set:Synchronized private var running: Boolean = false @@ -51,8 +59,7 @@ class NymTunnelManager @Inject constructor( override suspend fun start(fromBackground: Boolean) { runCatching { if (running) return Timber.w("Vpn already running") - val credentialExpiry = settingsRepository.getCredentialExpiry() - if (credentialExpiry.isInvalid()) return onInvalidCredential(credentialExpiry) + if (!isMnemonicStored()) return onMissingMnemonic() val intent = VpnService.prepare(context) if (intent != null) return launchVpnPermissionNotification() val entryCountry = settingsRepository.getFirstHopCountry() @@ -71,9 +78,23 @@ class NymTunnelManager @Inject constructor( } } - override suspend fun importCredential(credential: String): Result { - return kotlin.runCatching { - backend.get().importCredential(credential) + override suspend fun storeMnemonic(mnemonic: String) { + backend.get().storeMnemonic(mnemonic) + emitMnemonicStored(true) + } + + override suspend fun isMnemonicStored(): Boolean { + return backend.get().isMnemonicStored() + } + + override suspend fun removeMnemonic() { + backend.get().removeMnemonic() + emitMnemonicStored(false) + } + + private fun emitMnemonicStored(stored: Boolean) { + _state.update { + it.copy(isMnemonicStored = stored) } } @@ -104,16 +125,12 @@ class NymTunnelManager @Inject constructor( emitState(state) } - private fun onInvalidCredential(expiry: Instant?) { - val message = if (expiry == null) { - context.getString(R.string.missing_credential) - } else { - context.getString(R.string.exception_cred_invalid) - } + private fun onMissingMnemonic() { + val message = context.getString(R.string.missing_mnemonic) if (NymVpn.isForeground()) { emitMessage(BackendMessage.Failure(VpnException.InvalidCredential(details = message))) } else { - launchCredentialNotification(message) + launchMnemonicNotification(message) } } @@ -134,9 +151,9 @@ class NymTunnelManager @Inject constructor( } } - private fun launchCredentialNotification(description: String) { + private fun launchMnemonicNotification(description: String) { notificationService.showNotification( - title = context.getString(R.string.credential_failed_message), + title = context.getString(R.string.connection_failed), description = description, ) } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelManager.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelManager.kt index 25dd32b620..1b099e11b5 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelManager.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelManager.kt @@ -2,12 +2,13 @@ package net.nymtech.nymvpn.service.tunnel import kotlinx.coroutines.flow.Flow import net.nymtech.vpn.backend.Tunnel -import java.time.Instant interface TunnelManager { suspend fun stop() suspend fun start(fromBackground: Boolean = true) - suspend fun importCredential(credential: String): Result + suspend fun storeMnemonic(credential: String) + suspend fun isMnemonicStored(): Boolean + suspend fun removeMnemonic() val stateFlow: Flow fun getState(): Tunnel.State } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelState.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelState.kt index 3d4086907e..33a12c9a14 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelState.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/service/tunnel/TunnelState.kt @@ -8,4 +8,5 @@ data class TunnelState( val state: Tunnel.State = Tunnel.State.Down, val statistics: Statistics = Statistics(), val backendMessage: BackendMessage = BackendMessage.None, + val isMnemonicStored: Boolean = false, ) diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppUiState.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppUiState.kt index 3e12d0425f..c3304f6a18 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppUiState.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppUiState.kt @@ -10,4 +10,5 @@ data class AppUiState( val gateways: Gateways = Gateways(), val state: Tunnel.State = Tunnel.State.Down, val backendMessage: BackendMessage = BackendMessage.None, + val isMnemonicStored: Boolean = false, ) diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt index 0e7f3d810c..65281d8555 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt @@ -28,7 +28,7 @@ constructor( private val settingsRepository: SettingsRepository, private val gatewayRepository: GatewayRepository, @Native private val gatewayService: GatewayService, - tunnelManager: TunnelManager, + private val tunnelManager: TunnelManager, ) : ViewModel() { private val _navBarState = MutableStateFlow(NavBarState()) @@ -45,6 +45,7 @@ constructor( gateways, manager.state, manager.backendMessage, + isMnemonicStored = manager.isMnemonicStored, ) }.stateIn( viewModelScope, @@ -77,6 +78,10 @@ constructor( } } + fun logout() = viewModelScope.launch { + tunnelManager.removeMnemonic() + } + fun onErrorReportingSelected() = viewModelScope.launch { settingsRepository.setErrorReporting(!uiState.value.settings.errorReportingEnabled) } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/MainActivity.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/MainActivity.kt index 0631c5a261..7c785293e7 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/MainActivity.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/MainActivity.kt @@ -209,7 +209,6 @@ class MainActivity : ComponentActivity() { composable { SettingsScreen( appViewModel, - navController, appState, ) } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/common/buttons/ScaledSwitch.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/common/buttons/ScaledSwitch.kt index fc9250c354..8aa70d869e 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/common/buttons/ScaledSwitch.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/common/buttons/ScaledSwitch.kt @@ -1,18 +1,17 @@ package net.nymtech.nymvpn.ui.common.buttons -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp +import net.nymtech.nymvpn.util.extensions.scaledHeight @Composable -fun ScaledSwitch(checked: Boolean, modifier: Modifier = Modifier, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) { +fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) { Switch( checked, { onClick(it) }, - modifier = - modifier.padding(0.dp), - enabled = enabled, + Modifier.scale((52.dp.scaledHeight() / 52.dp)), ) } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/analytics/AnalyticsScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/analytics/AnalyticsScreen.kt index eb2c064c60..9a4ea6f44d 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/analytics/AnalyticsScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/analytics/AnalyticsScreen.kt @@ -174,10 +174,6 @@ fun AnalyticsScreen(appViewModel: AppViewModel, navController: NavController, ap ScaledSwitch( appUiState.settings.errorReportingEnabled, onClick = { appViewModel.onErrorReportingSelected() }, - modifier = - Modifier - .height(32.dp.scaledHeight()) - .width(52.dp.scaledWidth()), ) }, ), diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt index e11416e2d7..71b57d81ca 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt @@ -74,7 +74,6 @@ import net.nymtech.nymvpn.ui.theme.iconSize import net.nymtech.nymvpn.util.Constants import net.nymtech.nymvpn.util.extensions.buildCountryNameString import net.nymtech.nymvpn.util.extensions.goFromRoot -import net.nymtech.nymvpn.util.extensions.isInvalid import net.nymtech.nymvpn.util.extensions.openWebUrl import net.nymtech.nymvpn.util.extensions.scaledHeight import net.nymtech.nymvpn.util.extensions.scaledWidth @@ -321,7 +320,7 @@ fun MainScreen(appViewModel: AppViewModel, appUiState: AppUiState, autoStart: Bo testTag = Constants.CONNECT_TEST_TAG, onClick = { scope.launch { - if (appUiState.settings.credentialExpiry.isInvalid() + if (!appUiState.isMnemonicStored ) { return@launch navController.goFromRoot(Route.Credential) } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/permission/PermissionScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/permission/PermissionScreen.kt index 00b3a465ac..882ef290ae 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/permission/PermissionScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/permission/PermissionScreen.kt @@ -1,14 +1,18 @@ package net.nymtech.nymvpn.ui.screens.permission +import android.net.VpnService import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons @@ -37,6 +41,7 @@ import net.nymtech.nymvpn.ui.common.navigation.LocalNavController import net.nymtech.nymvpn.ui.common.navigation.NavBarState import net.nymtech.nymvpn.ui.common.navigation.NavIcon import net.nymtech.nymvpn.ui.common.navigation.NavTitle +import net.nymtech.nymvpn.ui.common.snackbar.SnackbarController import net.nymtech.nymvpn.ui.theme.CustomTypography import net.nymtech.nymvpn.util.extensions.launchVpnSettings import net.nymtech.nymvpn.util.extensions.navigateAndForget @@ -47,6 +52,7 @@ import net.nymtech.nymvpn.util.extensions.scaledWidth fun PermissionScreen(appViewModel: AppViewModel, permission: Permission) { val context = LocalContext.current val navController = LocalNavController.current + val snackbar = SnackbarController.current LaunchedEffect(Unit) { appViewModel.onNavBarStateChange( @@ -65,7 +71,7 @@ fun PermissionScreen(appViewModel: AppViewModel, permission: Permission) { modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp.scaledWidth()) - .padding(vertical = 24.dp), + .padding(vertical = 24.dp).windowInsetsPadding(WindowInsets.navigationBars), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { @@ -136,7 +142,11 @@ fun PermissionScreen(appViewModel: AppViewModel, permission: Permission) { Column(verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom)) { MainStyledButton( onClick = { - navController.navigateAndForget(Route.Main(true)) + if (VpnService.prepare(context) == null) { + navController.navigateAndForget(Route.Main(true)) + } else { + snackbar.showMessage(context.getString(R.string.permission_required)) + } }, content = { Text(stringResource(R.string.try_reconnecting), style = CustomTypography.labelHuge) }, ) diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerScreen.kt index 25b1866a3e..73404c8525 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerScreen.kt @@ -34,7 +34,7 @@ fun ScannerScreen(viewModel: ScannerViewModel = hiltViewModel()) { this.setStatusText("") this.decodeSingle { result -> result.text?.let { barCodeOrQr -> - viewModel.onCredentialImport(barCodeOrQr) + viewModel.onMnemonicImport(barCodeOrQr) } } } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerViewModel.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerViewModel.kt index 761a393fe1..9f344eae84 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerViewModel.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/scanner/ScannerViewModel.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import net.nymtech.nymvpn.R -import net.nymtech.nymvpn.data.SettingsRepository import net.nymtech.nymvpn.service.tunnel.TunnelManager import net.nymtech.nymvpn.ui.common.snackbar.SnackbarController import net.nymtech.nymvpn.util.StringValue @@ -17,26 +16,21 @@ import javax.inject.Inject @HiltViewModel class ScannerViewModel @Inject constructor( - private val settingsRepository: SettingsRepository, private val tunnelManager: TunnelManager, ) : ViewModel() { private val _success = MutableSharedFlow() val success = _success.asSharedFlow() - fun onCredentialImport(credential: String) = viewModelScope.launch { + fun onMnemonicImport(mnemonic: String) = viewModelScope.launch { runCatching { - tunnelManager.importCredential(credential).onSuccess { - Timber.d("Imported credential successfully") - it?.let { - settingsRepository.saveCredentialExpiry(it) - } - SnackbarController.showMessage(StringValue.StringResource(R.string.credential_successful)) - _success.emit(true) - }.onFailure { - SnackbarController.showMessage(StringValue.StringResource(R.string.credential_failed_message)) - _success.emit(false) - } + tunnelManager.storeMnemonic(mnemonic) + Timber.d("Imported account successfully") + SnackbarController.showMessage(StringValue.StringResource(R.string.device_added_success)) + _success.emit(true) + }.onFailure { + SnackbarController.showMessage(StringValue.StringResource(R.string.invalid_recovery_phrase)) + _success.emit(false) } } } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt index c3a2f5a6b8..62a55f161f 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/SettingsScreen.kt @@ -7,21 +7,23 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.material.icons.automirrored.outlined.ViewQuilt -import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.outlined.AdminPanelSettings import androidx.compose.material.icons.outlined.AppShortcut import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.material.icons.outlined.Launch import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,7 +42,6 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController import net.nymtech.nymvpn.BuildConfig import net.nymtech.nymvpn.R import net.nymtech.nymvpn.ui.AppUiState @@ -50,27 +51,24 @@ import net.nymtech.nymvpn.ui.common.buttons.MainStyledButton import net.nymtech.nymvpn.ui.common.buttons.ScaledSwitch import net.nymtech.nymvpn.ui.common.buttons.surface.SelectionItem import net.nymtech.nymvpn.ui.common.buttons.surface.SurfaceSelectionGroupButton +import net.nymtech.nymvpn.ui.common.navigation.LocalNavController import net.nymtech.nymvpn.ui.common.navigation.NavBarState import net.nymtech.nymvpn.ui.common.navigation.NavIcon import net.nymtech.nymvpn.ui.common.navigation.NavTitle +import net.nymtech.nymvpn.ui.common.snackbar.SnackbarController import net.nymtech.nymvpn.ui.theme.CustomTypography -import net.nymtech.nymvpn.util.extensions.durationFromNow -import net.nymtech.nymvpn.util.extensions.isInvalid +import net.nymtech.nymvpn.ui.theme.iconSize import net.nymtech.nymvpn.util.extensions.launchNotificationSettings import net.nymtech.nymvpn.util.extensions.launchVpnSettings import net.nymtech.nymvpn.util.extensions.openWebUrl -import net.nymtech.nymvpn.util.extensions.scaledHeight import net.nymtech.nymvpn.util.extensions.scaledWidth import net.nymtech.vpn.backend.Tunnel @Composable -fun SettingsScreen( - appViewModel: AppViewModel, - navController: NavController, - appUiState: AppUiState, - viewModel: SettingsViewModel = hiltViewModel(), -) { +fun SettingsScreen(appViewModel: AppViewModel, appUiState: AppUiState, viewModel: SettingsViewModel = hiltViewModel()) { val context = LocalContext.current + val snackbar = SnackbarController.current + val navController = LocalNavController.current val clipboardManager: ClipboardManager = LocalClipboardManager.current val padding = WindowInsets.systemBars.asPaddingValues() @@ -97,49 +95,33 @@ fun SettingsScreen( .padding(top = 24.dp) .padding(horizontal = 24.dp.scaledWidth()).padding(bottom = padding.calculateBottomPadding()), ) { - if (appUiState.settings.credentialExpiry.isInvalid()) { + if (!appUiState.isMnemonicStored) { MainStyledButton( onClick = { navController.navigate(Route.Credential) }, content = { Text( - stringResource(id = R.string.add_cred_to_connect), + stringResource(id = R.string.log_in), style = CustomTypography.labelHuge, ) }, color = MaterialTheme.colorScheme.primary, ) } else { - appUiState.settings.credentialExpiry?.let { - val credentialDuration = it.durationFromNow() - val days = credentialDuration.toDaysPart() - val hours = credentialDuration.toHoursPart() - val accountDescription = - buildAnnotatedString { - if (days != 0L) { - append(days.toString()) - append(" ") - append(if (days != 1L) stringResource(id = R.string.days) else stringResource(id = R.string.day)) - } else { - append(hours.toString()) - append(" ") - append(if (hours != 1) stringResource(id = R.string.hours) else stringResource(id = R.string.hour)) - } - append(" ") - append(stringResource(id = R.string.remaining)) - } - SurfaceSelectionGroupButton( - listOf( - SelectionItem( - Icons.Filled.AccountCircle, - onClick = { - navController.navigate(Route.Account) - }, - title = { Text(stringResource(R.string.credential), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, - description = { Text(accountDescription.text, style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.outline)) }, - ), + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + Icons.Outlined.Person, + { + val icon = Icons.AutoMirrored.Outlined.Launch + Icon(icon, icon.name, Modifier.size(iconSize)) + }, + title = { Text(stringResource(R.string.account), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + context.openWebUrl(context.getString(R.string.account_url)) + }, ), - ) - } + ), + ) } SurfaceSelectionGroupButton( listOf( @@ -149,10 +131,6 @@ fun SettingsScreen( ScaledSwitch( appUiState.settings.autoStartEnabled, onClick = { viewModel.onAutoConnectSelected(it) }, - modifier = - Modifier - .height(32.dp.scaledHeight()) - .width(52.dp.scaledWidth()), ) }, title = { Text(stringResource(R.string.auto_connect), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, @@ -176,10 +154,6 @@ fun SettingsScreen( ScaledSwitch( appUiState.settings.firstHopSelectionEnabled, onClick = { appViewModel.onEntryLocationSelected(it) }, - modifier = - Modifier - .height(32.dp.scaledHeight()) - .width(52.dp.scaledWidth()), enabled = (appUiState.state is Tunnel.State.Down), ) }, @@ -215,10 +189,6 @@ fun SettingsScreen( ScaledSwitch( appUiState.settings.isShortcutsEnabled, onClick = { checked -> viewModel.onAppShortcutsSelected(checked) }, - modifier = - Modifier - .height(32.dp.scaledHeight()) - .width(52.dp.scaledWidth()), ) }, title = { Text(stringResource(R.string.app_shortcuts), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, @@ -281,10 +251,6 @@ fun SettingsScreen( ScaledSwitch( checked = appUiState.settings.errorReportingEnabled, onClick = { appViewModel.onErrorReportingSelected() }, - modifier = - Modifier - .height(32.dp.scaledHeight()) - .width(52.dp.scaledWidth()), ) }, ), @@ -324,6 +290,23 @@ fun SettingsScreen( ), ), ) + if (appUiState.isMnemonicStored) { + SurfaceSelectionGroupButton( + listOf( + SelectionItem( + title = { Text(stringResource(R.string.log_out), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) }, + onClick = { + if (appUiState.state == Tunnel.State.Down) { + appViewModel.logout() + } else { + snackbar.showMessage(context.getString(R.string.action_requires_tunnel_down)) + } + }, + trailing = {}, + ), + ), + ) + } Column( verticalArrangement = Arrangement.Bottom, horizontalAlignment = Alignment.Start, diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/account/AccountScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/account/AccountScreen.kt index 49584b14e4..5d92786eda 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/account/AccountScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/account/AccountScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,7 +27,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import net.nymtech.nymvpn.R import net.nymtech.nymvpn.ui.AppUiState @@ -44,8 +42,6 @@ import net.nymtech.nymvpn.ui.common.navigation.NavIcon import net.nymtech.nymvpn.ui.common.navigation.NavTitle import net.nymtech.nymvpn.ui.screens.settings.account.model.Device import net.nymtech.nymvpn.ui.theme.CustomTypography -import net.nymtech.nymvpn.util.Constants -import net.nymtech.nymvpn.util.extensions.durationFromNow import net.nymtech.nymvpn.util.extensions.scaledHeight import net.nymtech.nymvpn.util.extensions.scaledWidth import net.nymtech.nymvpn.util.extensions.showToast @@ -90,36 +86,36 @@ fun AccountScreen(appViewModel: AppViewModel, appUiState: AppUiState) { horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxSize(), ) { - appUiState.settings.credentialExpiry?.let { - val credentialDuration = it.durationFromNow() - val days = credentialDuration.toDaysPart() - val hours = credentialDuration.toHoursPart() - val durationLeft = - buildAnnotatedString { - append(days.toString()) - append(" ") - append(if (days != 1L) stringResource(id = R.string.days) else stringResource(id = R.string.day)) - append(", ") - append(hours.toString()) - append(" ") - append(if (hours != 1) stringResource(id = R.string.hours) else stringResource(id = R.string.hour)) - append(" ") - append(stringResource(id = R.string.remaining)) - } - Text( - durationLeft.text, - style = CustomTypography.labelHuge, - color = MaterialTheme.colorScheme.onSurface, - ) - LinearProgressIndicator( - modifier = - Modifier - .fillMaxWidth(), - progress = { - days.toFloat() / Constants.FREE_PASS_CRED_DURATION - }, - ) - } +// appUiState.settings.credentialExpiry?.let { +// val credentialDuration = it.durationFromNow() +// val days = credentialDuration.toDaysPart() +// val hours = credentialDuration.toHoursPart() +// val durationLeft = +// buildAnnotatedString { +// append(days.toString()) +// append(" ") +// append(if (days != 1L) stringResource(id = R.string.days) else stringResource(id = R.string.day)) +// append(", ") +// append(hours.toString()) +// append(" ") +// append(if (hours != 1) stringResource(id = R.string.hours) else stringResource(id = R.string.hour)) +// append(" ") +// append(stringResource(id = R.string.remaining)) +// } +// Text( +// durationLeft.text, +// style = CustomTypography.labelHuge, +// color = MaterialTheme.colorScheme.onSurface, +// ) +// LinearProgressIndicator( +// modifier = +// Modifier +// .fillMaxWidth(), +// progress = { +// days.toFloat() / Constants.FREE_PASS_CRED_DURATION +// }, +// ) +// } Row( horizontalArrangement = Arrangement.SpaceBetween, diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/appearance/language/LanguageScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/appearance/language/LanguageScreen.kt index 580e47eaf5..ab51477080 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/appearance/language/LanguageScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/appearance/language/LanguageScreen.kt @@ -1,8 +1,11 @@ package net.nymtech.nymvpn.ui.screens.settings.appearance.language import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons @@ -87,7 +90,7 @@ fun LanguageScreen(appViewModel: AppViewModel, localeStorage: LocaleStorage) { Modifier .fillMaxSize() .padding(top = 24.dp.scaledHeight()) - .padding(horizontal = 24.dp.scaledWidth()), + .padding(horizontal = 24.dp.scaledWidth()).windowInsetsPadding(WindowInsets.navigationBars), ) { item { SelectionItemButton( diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialScreen.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialScreen.kt index df8e0d2b9b..d4ebeca4c6 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialScreen.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialScreen.kt @@ -4,7 +4,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets @@ -14,14 +13,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.QrCodeScanner -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -36,7 +32,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -51,9 +50,9 @@ import net.nymtech.nymvpn.ui.common.navigation.NavBarState import net.nymtech.nymvpn.ui.common.snackbar.SnackbarController import net.nymtech.nymvpn.ui.common.textbox.CustomTextField import net.nymtech.nymvpn.ui.theme.CustomTypography -import net.nymtech.nymvpn.ui.theme.iconSize import net.nymtech.nymvpn.util.Constants import net.nymtech.nymvpn.util.extensions.navigateAndForget +import net.nymtech.nymvpn.util.extensions.openWebUrl import net.nymtech.nymvpn.util.extensions.scaledHeight import net.nymtech.nymvpn.util.extensions.scaledWidth @@ -67,7 +66,6 @@ fun CredentialScreen(appViewModel: AppViewModel, viewModel: CredentialViewModel val context = LocalContext.current val navController = LocalNavController.current - val error = viewModel.error.collectAsStateWithLifecycle() val success = viewModel.success.collectAsStateWithLifecycle(null) LaunchedEffect(success.value) { @@ -91,7 +89,7 @@ fun CredentialScreen(appViewModel: AppViewModel, viewModel: CredentialViewModel ) } - var credential by remember { + var mnemonic by remember { mutableStateOf("") } @@ -136,13 +134,13 @@ fun CredentialScreen(appViewModel: AppViewModel, viewModel: CredentialViewModel color = MaterialTheme.colorScheme.onBackground, ) Text( - text = stringResource(id = R.string.credential_message), + text = stringResource(id = R.string.recovery_phrase_message), style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center, ) Text( - text = stringResource(id = R.string.credential_disclaimer), + text = stringResource(id = R.string.recovery_phrase_disclaimer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, @@ -153,30 +151,28 @@ fun CredentialScreen(appViewModel: AppViewModel, viewModel: CredentialViewModel verticalArrangement = Arrangement.spacedBy(32.dp.scaledHeight(), Alignment.Top), ) { CustomTextField( - value = credential, + placeholder = { + Text(stringResource(R.string.mnemonic_example), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + }, + value = mnemonic, onValueChange = { - if (error.value != null) viewModel.resetError() - credential = it + if (success.value == false) viewModel.resetSuccess() + mnemonic = it }, modifier = Modifier .width(358.dp.scaledWidth()) .height(212.dp.scaledHeight()), supportingText = { - if (error.value != null) { - // TODO need a better way to determine this in the future + if (success.value == false) { Text( modifier = Modifier.fillMaxWidth(), - text = if (error.value!!.contains("unique constraint violation")) { - stringResource(R.string.credential_already_imported) - } else { - stringResource(R.string.credential_failed_message) - }, + text = stringResource(R.string.invalid_recovery_phrase), color = MaterialTheme.colorScheme.error, ) } }, - isError = error.value != null, - label = { Text(text = stringResource(id = R.string.credential_label)) }, + isError = success.value == false, + label = { Text(text = stringResource(id = R.string.recovery_phrase), color = MaterialTheme.colorScheme.onSurface) }, textStyle = MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurface, ), @@ -187,32 +183,60 @@ fun CredentialScreen(appViewModel: AppViewModel, viewModel: CredentialViewModel .fillMaxWidth() .padding(bottom = 24.dp.scaledHeight()), ) { - Box(modifier = Modifier.width(286.dp.scaledWidth())) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Box(modifier = Modifier.width(286.dp.scaledWidth())) { MainStyledButton( Constants.LOGIN_TEST_TAG, onClick = { - viewModel.onImportCredential(credential) + viewModel.onMnemonicImport(mnemonic) }, content = { Text( - stringResource(id = R.string.add_credential), + stringResource(id = R.string.log_in), style = CustomTypography.labelHuge, ) }, color = MaterialTheme.colorScheme.primary, ) - } - Box(modifier = Modifier.width(56.dp.scaledWidth())) { - MainStyledButton( - onClick = { - requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) - }, - content = { - val icon = Icons.Outlined.QrCodeScanner - Icon(icon, icon.name, modifier = Modifier.size(iconSize.scaledWidth())) - }, - color = MaterialTheme.colorScheme.primary, - ) + +// Disable scanner for now + // } +// Box(modifier = Modifier.width(56.dp.scaledWidth())) { +// MainStyledButton( +// onClick = { +// requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) +// }, +// content = { +// val icon = Icons.Outlined.QrCodeScanner +// Icon(icon, icon.name, modifier = Modifier.size(iconSize.scaledWidth())) +// }, +// color = MaterialTheme.colorScheme.primary, +// ) +// } + val createAccountMessage = buildAnnotatedString { + append(stringResource(id = R.string.new_to_nym)) + append(" ") + pushStringAnnotation(tag = "create", annotation = stringResource(id = R.string.create_account_link)) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.create_account)) + } + pop() + } + ClickableText( + text = createAccountMessage, + style = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + ), + modifier = Modifier.padding(bottom = 24.dp.scaledHeight()), + ) { + createAccountMessage.getStringAnnotations(tag = "create", it, it).firstOrNull()?.let { annotation -> + context.openWebUrl(annotation.item) + } + } } } } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialViewModel.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialViewModel.kt index 3cbf48987d..8dd86a702c 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialViewModel.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/credential/CredentialViewModel.kt @@ -4,12 +4,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import net.nymtech.nymvpn.R -import net.nymtech.nymvpn.data.SettingsRepository import net.nymtech.nymvpn.service.tunnel.TunnelManager import net.nymtech.nymvpn.ui.common.snackbar.SnackbarController import net.nymtech.nymvpn.util.StringValue @@ -20,32 +17,23 @@ import javax.inject.Inject class CredentialViewModel @Inject constructor( - private val settingsRepository: SettingsRepository, private val tunnelManager: TunnelManager, ) : ViewModel() { - private val _error = MutableStateFlow(null) - val error = _error.asStateFlow() - - private val _success = MutableSharedFlow() + private val _success = MutableSharedFlow() val success = _success.asSharedFlow() - fun onImportCredential(credential: String) = viewModelScope.launch { - val trimmedCred = credential.trim() - tunnelManager.importCredential(trimmedCred).onSuccess { - Timber.d("Imported credential successfully") - it?.let { - settingsRepository.saveCredentialExpiry(it) - } - SnackbarController.showMessage(StringValue.StringResource(R.string.credential_successful)) + fun onMnemonicImport(mnemonic: String) = viewModelScope.launch { + runCatching { + tunnelManager.storeMnemonic(mnemonic.trim()) + Timber.d("Imported account successfully") + SnackbarController.showMessage(StringValue.StringResource(R.string.device_added_success)) _success.emit(true) }.onFailure { - _error.emit(it.message) - Timber.e(it) + _success.emit(false) } } - - fun resetError() { - _error.tryEmit(null) + fun resetSuccess() = viewModelScope.launch { + _success.emit(null) } } diff --git a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/theme/Size.kt b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/theme/Size.kt index 2ef5a165f1..0a1634f262 100644 --- a/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/theme/Size.kt +++ b/nym-vpn-android/app/src/main/java/net/nymtech/nymvpn/ui/theme/Size.kt @@ -1,5 +1,6 @@ package net.nymtech.nymvpn.ui.theme import androidx.compose.ui.unit.dp +import net.nymtech.nymvpn.util.extensions.scaledHeight -val iconSize = 24.dp +val iconSize = 24.dp.scaledHeight() diff --git a/nym-vpn-android/app/src/main/res/values/strings.xml b/nym-vpn-android/app/src/main/res/values/strings.xml index 5d76d2466e..c58812f13a 100644 --- a/nym-vpn-android/app/src/main/res/values/strings.xml +++ b/nym-vpn-android/app/src/main/res/values/strings.xml @@ -176,4 +176,18 @@ Action requires tunnel down Selected exit country is unavailable. Please select a different exit country. Selected entry country is unavailable. Please select a different entry country. + Missing mnemonic + Device successfully added to your account + Invalid recovery phrase + Recovery phrase + Log in + Log out + New to NymVPN? + https://nymvpn.com/account/create + Create an account + Account + https://nymvpn.com/account/login + e.g. smoke artefact velvet skull pop palace tortoise damage rough... + Enter your 24-word secret recovery phrase below to log in to your account + You can only use the recovery phrase created in NymVPN,
not the Nym Wallet. diff --git a/nym-vpn-android/gradle/libs.versions.toml b/nym-vpn-android/gradle/libs.versions.toml index 35f6f902f9..fe47e3db49 100644 --- a/nym-vpn-android/gradle/libs.versions.toml +++ b/nym-vpn-android/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] accompanist = "0.36.0" -agp = "8.7.0" +agp = "8.7.1" coreSplashscreen = "1.0.1" detektRulesCompose = "1.4.0" ipaddress = "5.5.1" jna = "5.15.0" -kotlin = "2.0.20" -ksp = "2.0.20-1.0.25" +kotlin = "2.0.21" +ksp = "2.0.21-1.0.25" coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.2.1" @@ -20,7 +20,7 @@ composeBom = "2024.09.03" datastorePreferences = "1.1.1" relinker = "1.4.5" securityCrypto = "1.1.0-alpha06" -sentryOpentelemetryCoreVersion = "7.14.0" +sentryOpentelemetryCoreVersion = "7.15.0" timber = "5.0.1" hiltAndroid = "2.52" kotlinx-serialization-json = "1.7.3" @@ -35,7 +35,6 @@ moshiKotlinCodegen = "1.15.1" converterMoshi = "2.11.0" zxingAndroidEmbedded = "4.3.0" -gradlePlugins-kotlinxSerialization = "2.0.20" gradlePlugins-licensee = "1.11.0" gradlePlugins-detekt = "1.23.7" gradlePlugins-gross = "0.4.2" @@ -117,7 +116,7 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } android-library = { id = "com.android.library", version.ref = "agp" } licensee = { id = "app.cash.licensee", version.ref = "gradlePlugins-licensee" } gross = { id = "se.premex.gross", version.ref = "gradlePlugins-gross" } -kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "gradlePlugins-kotlinxSerialization" } +kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "gradlePlugins-detekt" } sentry = { id = "io.sentry.android.gradle", version.ref = "gradlePlugins-sentry" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "gradlePlugins-ktlint" } diff --git a/nym-vpn-android/nym-vpn-client/src/main/AndroidManifest.xml b/nym-vpn-android/nym-vpn-client/src/main/AndroidManifest.xml index c0f31ec9e8..4702d0f920 100644 --- a/nym-vpn-android/nym-vpn-client/src/main/AndroidManifest.xml +++ b/nym-vpn-android/nym-vpn-client/src/main/AndroidManifest.xml @@ -9,7 +9,6 @@ - Result<(), VpnErr } #[allow(non_snake_case)] +#[uniffi::export] pub fn isAccountMnemonicStored(path: String) -> Result { RUNTIME.block_on(is_account_mnemonic_stored(&path)) } @@ -367,6 +368,7 @@ async fn is_account_mnemonic_stored(path: &str) -> Result { } #[allow(non_snake_case)] +#[uniffi::export] pub fn removeAccountMnemonic(path: String) -> Result { RUNTIME.block_on(remove_account_mnemonic(&path)) }