OXYGENHUB
# 62429abe5869 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / signin / SignInViewModel.kt
3.6 KiB · last changed Sign-in, dashboard, repositories, issues and inbox screens, adaptive (refs #2) 11 days ago source blame history raw
1
package io.oxygenhub.android.ui.signin
2
3
import androidx.lifecycle.ViewModel
4
import androidx.lifecycle.viewModelScope
5
import io.oxygenhub.android.api.DeviceAuth
6
import io.oxygenhub.android.api.DeviceCode
7
import io.oxygenhub.android.api.DeviceFlowException
8
import io.oxygenhub.android.api.HubException
9
import io.oxygenhub.android.api.normalizeHubUrl
10
import io.oxygenhub.android.api.userMessage
11
import io.oxygenhub.android.auth.Session
12
import io.oxygenhub.android.auth.SessionManager
13
import kotlinx.coroutines.CancellationException
14
import kotlinx.coroutines.Job
15
import kotlinx.coroutines.flow.MutableStateFlow
16
import kotlinx.coroutines.flow.StateFlow
17
import kotlinx.coroutines.flow.asStateFlow
18
import kotlinx.coroutines.launch
19
20
sealed interface SignInState {
21
    /** Ready to start; [error] says why the last try ended. */
22
    data class Idle(val error: String? = null) : SignInState
23
24
    /** Asking the hub for a code. */
25
    data object Requesting : SignInState
26
27
    /** Showing the code while the user approves it on the web. */
28
    data class Approving(val code: DeviceCode) : SignInState
29
30
    /** Approved: checking the token and saving it. */
31
    data object Finishing : SignInState
32
}
33
34
class SignInViewModel(
35
    private val sessions: SessionManager,
36
    private val authFor: (hub: String) -> DeviceAuth,
37
    private val clientName: String,
38
    defaultHub: String,
39
) : ViewModel() {
40
    private val _hub = MutableStateFlow(sessions.lastHub ?: defaultHub)
41
    val hub: StateFlow<String> = _hub.asStateFlow()
42
43
    private val _state = MutableStateFlow<SignInState>(SignInState.Idle())
44
    val state: StateFlow<SignInState> = _state.asStateFlow()
45
46
    /** Why the last session ended, if the hub ended it. */
47
    val ended: StateFlow<String?> = sessions.ended
48
49
    private var job: Job? = null
50
51
    fun setHub(value: String) {
52
        _hub.value = value
53
        if (_state.value is SignInState.Idle) _state.value = SignInState.Idle()
54
    }
55
56
    fun signIn() {
57
        if (job?.isActive == true) return
58
        val hub = normalizeHubUrl(_hub.value)
59
        if (hub == null) {
60
            _state.value = SignInState.Idle("Enter the hub's HTTPS address, such as https://oxygenhub.io.")
61
            return
62
        }
63
        _hub.value = hub
64
        sessions.rememberHub(hub)
65
        sessions.dismissEnded()
66
        job = viewModelScope.launch {
67
            _state.value = SignInState.Requesting
68
            try {
69
                val auth = authFor(hub)
70
                val code = auth.requestCode(clientName)
71
                _state.value = SignInState.Approving(code)
72
                val grant = auth.awaitToken(code)
73
                _state.value = SignInState.Finishing
74
                val username = try {
75
                    auth.whoAmI(grant.accessToken).username
76
                } catch (e: HubException.Http) {
77
                    // Approved without user:read: the token's answer names the account too.
78
                    if (e.status == 403 && grant.username.isNotBlank()) grant.username else throw e
79
                }
80
                sessions.signedIn(Session(hub, username, grant.accessToken, grant.scopes))
81
                _state.value = SignInState.Idle()
82
            } catch (e: CancellationException) {
83
                throw e
84
            } catch (e: DeviceFlowException) {
85
                _state.value = SignInState.Idle(e.message)
86
            } catch (e: Exception) {
87
                _state.value = SignInState.Idle(e.userMessage())
88
            }
89
        }
90
    }
91
92
    /** Stops waiting. The code is left to expire on the hub (it has no call to withdraw one). */
93
    fun cancel() {
94
        job?.cancel()
95
        job = null
96
        _state.value = SignInState.Idle()
97
    }
98
}