1package com.parseable.android.ui.screens.login
3import androidx.lifecycle.ViewModel
4import androidx.lifecycle.viewModelScope
5import com.parseable.android.data.model.ApiResult
6import com.parseable.android.data.model.ServerConfig
7import com.parseable.android.data.repository.ParseableRepository
8import com.parseable.android.data.repository.SettingsRepository
9import dagger.hilt.android.lifecycle.HiltViewModel
10import kotlinx.coroutines.flow.MutableStateFlow
11import kotlinx.coroutines.flow.StateFlow
12import kotlinx.coroutines.flow.asStateFlow
13import kotlinx.coroutines.flow.first
14import kotlinx.coroutines.flow.update
15import kotlinx.coroutines.launch
16import javax.inject.Inject
19 val serverUrl: String = "",
20 val username: String = "",
21 val password: String = "",
22 val allowInsecure: Boolean = false,
23 val isLoading: Boolean = false,
24 val error: String? = null,
25 val loginSuccess: Boolean = false,
26 val serverUrlError: String? = null,
27 val usernameError: String? = null,
28 val passwordError: String? = null,
29 val hasSavedCredentials: Boolean = false,
33class LoginViewModel @Inject constructor(
34 private val repository: ParseableRepository,
35 private val settingsRepository: SettingsRepository,
38 private val _state = MutableStateFlow(LoginState())
39 val state: StateFlow<LoginState> = _state.asStateFlow()
42 viewModelScope.launch {
43 val config = settingsRepository.serverConfig.first()
47 serverUrl = config.serverUrl,
48 username = config.username,
49 allowInsecure = !config.useTls,
50 hasSavedCredentials = true,
57 fun loginWithSavedCredentials() {
58 viewModelScope.launch {
59 val config = settingsRepository.getSavedPassword() ?: return@launch
60 performLogin(config.password)
64 fun onServerUrlChange(value: String) {
65 _state.update { it.copy(serverUrl = value, serverUrlError = null, error = null) }
68 fun onUsernameChange(value: String) {
69 _state.update { it.copy(username = value, usernameError = null, error = null) }
72 fun onPasswordChange(value: String) {
73 _state.update { it.copy(password = value, passwordError = null, error = null) }
76 fun onAllowInsecureChange(value: Boolean) {
77 _state.update { it.copy(allowInsecure = value) }
81 val current = _state.value
85 var urlError: String? = null
86 var userError: String? = null
87 var passError: String? = null
89 if (current.serverUrl.isBlank()) {
90 urlError = "Server URL is required"
93 if (current.username.isBlank()) {
94 userError = "Username is required"
97 if (current.password.isBlank()) {
98 passError = "Password is required"
105 serverUrlError = urlError,
106 usernameError = userError,
107 passwordError = passError,
113 viewModelScope.launch {
114 performLogin(current.password)
118 private suspend fun performLogin(password: String) {
119 val current = _state.value
120 _state.update { it.copy(isLoading = true, error = null) }
122 var url = current.serverUrl.trim()
123 val urlLower = url.lowercase()
124 if (!urlLower.startsWith("http://") && !urlLower.startsWith("https://")) {
125 url = if (current.allowInsecure) "http://$url" else "https://$url"
128 val isValidUrl = try {
129 val uri = java.net.URI(url)
130 uri.scheme != null && !uri.host.isNullOrBlank() && !uri.host!!.contains(" ")
131 } catch (_: Exception) {
138 serverUrlError = "Invalid URL format",
144 val config = ServerConfig(
146 username = current.username.trim(),
148 useTls = url.lowercase().startsWith("https"),
151 repository.configure(config)
153 when (val result = repository.testConnection()) {
154 is ApiResult.Success -> {
155 // Verify this is actually a Parseable server and credentials are valid.
156 // Uses verifyServer() to avoid triggering the global auth-error handler.
157 when (val aboutResult = repository.verifyServer()) {
158 is ApiResult.Success -> {
159 settingsRepository.saveServerConfig(config)
160 _state.update { it.copy(isLoading = false, loginSuccess = true, password = "") }
162 is ApiResult.Error -> {
163 val errorMsg = if (aboutResult.isUnauthorized) {
164 "Invalid credentials. Check your username and password."
166 "Server responded but doesn't appear to be Parseable: ${aboutResult.userMessage}"
169 it.copy(isLoading = false, error = errorMsg)
174 is ApiResult.Error -> {
178 error = "Connection failed: ${result.userMessage}",
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list