git.iko.soy/parseable-android
on e7bbc3d help sign in
184 lines 6.2 kB history raw
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
18data class LoginState(
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,
32@HiltViewModel
33class LoginViewModel @Inject constructor(
34 private val repository: ParseableRepository,
35 private val settingsRepository: SettingsRepository,
36) : ViewModel() {
38 private val _state = MutableStateFlow(LoginState())
39 val state: StateFlow<LoginState> = _state.asStateFlow()
41 init {
42 viewModelScope.launch {
43 val config = settingsRepository.serverConfig.first()
44 if (config != null) {
45 _state.update {
46 it.copy(
47 serverUrl = config.serverUrl,
48 username = config.username,
49 allowInsecure = !config.useTls,
50 hasSavedCredentials = true,
51 )
52 }
53 }
54 }
55 }
57 fun loginWithSavedCredentials() {
58 viewModelScope.launch {
59 val config = settingsRepository.getSavedPassword() ?: return@launch
60 performLogin(config.password)
61 }
62 }
64 fun onServerUrlChange(value: String) {
65 _state.update { it.copy(serverUrl = value, serverUrlError = null, error = null) }
66 }
68 fun onUsernameChange(value: String) {
69 _state.update { it.copy(username = value, usernameError = null, error = null) }
70 }
72 fun onPasswordChange(value: String) {
73 _state.update { it.copy(password = value, passwordError = null, error = null) }
74 }
76 fun onAllowInsecureChange(value: Boolean) {
77 _state.update { it.copy(allowInsecure = value) }
78 }
80 fun onLogin() {
81 val current = _state.value
83 // Validate
84 var hasError = false
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"
91 hasError = true
92 }
93 if (current.username.isBlank()) {
94 userError = "Username is required"
95 hasError = true
96 }
97 if (current.password.isBlank()) {
98 passError = "Password is required"
99 hasError = true
100 }
102 if (hasError) {
103 _state.update {
104 it.copy(
105 serverUrlError = urlError,
106 usernameError = userError,
107 passwordError = passError,
108 )
109 }
110 return
111 }
113 viewModelScope.launch {
114 performLogin(current.password)
115 }
116 }
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"
126 }
128 val isValidUrl = try {
129 val uri = java.net.URI(url)
130 uri.scheme != null && !uri.host.isNullOrBlank() && !uri.host!!.contains(" ")
131 } catch (_: Exception) {
132 false
133 }
134 if (!isValidUrl) {
135 _state.update {
136 it.copy(
137 isLoading = false,
138 serverUrlError = "Invalid URL format",
139 )
140 }
141 return
142 }
144 val config = ServerConfig(
145 serverUrl = url,
146 username = current.username.trim(),
147 password = password,
148 useTls = url.lowercase().startsWith("https"),
149 )
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 = "") }
161 }
162 is ApiResult.Error -> {
163 val errorMsg = if (aboutResult.isUnauthorized) {
164 "Invalid credentials. Check your username and password."
165 } else {
166 "Server responded but doesn't appear to be Parseable: ${aboutResult.userMessage}"
167 }
168 _state.update {
169 it.copy(isLoading = false, error = errorMsg)
170 }
171 }
172 }
173 }
174 is ApiResult.Error -> {
175 _state.update {
176 it.copy(
177 isLoading = false,
178 error = "Connection failed: ${result.userMessage}",
179 )
180 }
181 }
182 }
183 }