OXYGENHUB
# 75ceba747960 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / signin / SignInScreen.kt
7.5 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.compose.foundation.Image
4
import androidx.compose.foundation.layout.Arrangement
5
import androidx.compose.foundation.layout.Box
6
import androidx.compose.foundation.layout.Column
7
import androidx.compose.foundation.layout.Spacer
8
import androidx.compose.foundation.layout.fillMaxSize
9
import androidx.compose.foundation.layout.fillMaxWidth
10
import androidx.compose.foundation.layout.height
11
import androidx.compose.foundation.layout.imePadding
12
import androidx.compose.foundation.layout.padding
13
import androidx.compose.foundation.layout.safeDrawingPadding
14
import androidx.compose.foundation.layout.size
15
import androidx.compose.foundation.layout.widthIn
16
import androidx.compose.foundation.rememberScrollState
17
import androidx.compose.foundation.text.KeyboardActions
18
import androidx.compose.foundation.text.KeyboardOptions
19
import androidx.compose.foundation.verticalScroll
20
import androidx.compose.material.icons.Icons
21
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
22
import androidx.compose.material.icons.outlined.Dns
23
import androidx.compose.material3.Button
24
import androidx.compose.material3.Card
25
import androidx.compose.material3.CardDefaults
26
import androidx.compose.material3.CircularProgressIndicator
27
import androidx.compose.material3.Icon
28
import androidx.compose.material3.LinearProgressIndicator
29
import androidx.compose.material3.MaterialTheme
30
import androidx.compose.material3.OutlinedTextField
31
import androidx.compose.material3.Surface
32
import androidx.compose.material3.Text
33
import androidx.compose.material3.TextButton
34
import androidx.compose.runtime.Composable
35
import androidx.compose.runtime.getValue
36
import androidx.compose.ui.Alignment
37
import androidx.compose.ui.Modifier
38
import androidx.compose.ui.platform.LocalUriHandler
39
import androidx.compose.ui.res.painterResource
40
import androidx.compose.ui.semantics.contentDescription
41
import androidx.compose.ui.semantics.semantics
42
import androidx.compose.ui.text.font.FontFamily
43
import androidx.compose.ui.text.font.FontWeight
44
import androidx.compose.ui.text.input.ImeAction
45
import androidx.compose.ui.text.input.KeyboardType
46
import androidx.compose.ui.text.style.TextAlign
47
import androidx.compose.ui.unit.dp
48
import androidx.compose.ui.unit.sp
49
import androidx.lifecycle.compose.collectAsStateWithLifecycle
50
import io.oxygenhub.android.R
51
import io.oxygenhub.android.api.DeviceCode
52
53
@Composable
54
fun SignInScreen(vm: SignInViewModel) {
55
    val hub by vm.hub.collectAsStateWithLifecycle()
56
    val state by vm.state.collectAsStateWithLifecycle()
57
    val ended by vm.ended.collectAsStateWithLifecycle()
58
    SignInContent(
59
        hub = hub,
60
        state = state,
61
        notice = ended,
62
        onHubChange = vm::setHub,
63
        onSignIn = vm::signIn,
64
        onCancel = vm::cancel,
65
    )
66
}
67
68
@Composable
69
fun SignInContent(
70
    hub: String,
71
    state: SignInState,
72
    notice: String?,
73
    onHubChange: (String) -> Unit,
74
    onSignIn: () -> Unit,
75
    onCancel: () -> Unit,
76
) {
77
    Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
78
        Box(Modifier.fillMaxSize().safeDrawingPadding().imePadding(), contentAlignment = Alignment.Center) {
79
            Column(
80
                Modifier
81
                    .widthIn(max = 480.dp)
82
                    .fillMaxWidth()
83
                    .verticalScroll(rememberScrollState())
84
                    .padding(24.dp),
85
                horizontalAlignment = Alignment.CenterHorizontally,
86
            ) {
87
                Image(painterResource(R.drawable.mark), contentDescription = null, modifier = Modifier.size(width = 96.dp, height = 48.dp))
88
                Spacer(Modifier.height(16.dp))
89
                Text("OxygenHub", fontSize = 36.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
90
                Spacer(Modifier.height(32.dp))
91
                when (state) {
92
                    is SignInState.Idle -> Start(hub, state.error ?: notice, onHubChange, onSignIn)
93
                    SignInState.Requesting -> Waiting("Asking $hub for a code…")
94
                    is SignInState.Approving -> Approve(state.code, onCancel)
95
                    SignInState.Finishing -> Waiting("Approved. Signing in…")
96
                }
97
            }
98
        }
99
    }
100
}
101
102
@Composable
103
private fun Start(hub: String, error: String?, onHubChange: (String) -> Unit, onSignIn: () -> Unit) {
104
    Text(
105
        "Sign in with your browser: the app shows a code, you approve it on the hub's web page, " +
106
            "signed in there with your password and second factor. The app never sees either.",
107
        style = MaterialTheme.typography.bodyLarge,
108
        color = MaterialTheme.colorScheme.onSurfaceVariant,
109
        textAlign = TextAlign.Center,
110
    )
111
    Spacer(Modifier.height(24.dp))
112
    OutlinedTextField(
113
        value = hub,
114
        onValueChange = onHubChange,
115
        label = { Text("Hub") },
116
        leadingIcon = { Icon(Icons.Outlined.Dns, contentDescription = null) },
117
        singleLine = true,
118
        isError = error != null,
119
        supportingText = { Text(error ?: "The hub's address; oxygenhub.io unless yours is your own.") },
120
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Go, autoCorrectEnabled = false),
121
        keyboardActions = KeyboardActions(onGo = { onSignIn() }),
122
        modifier = Modifier.fillMaxWidth(),
123
    )
124
    Spacer(Modifier.height(16.dp))
125
    Button(onClick = onSignIn, enabled = hub.isNotBlank(), modifier = Modifier.fillMaxWidth().height(50.dp)) {
126
        Text("Sign in", fontSize = 16.sp)
127
    }
128
}
129
130
@Composable
131
private fun Waiting(what: String) {
132
    CircularProgressIndicator()
133
    Spacer(Modifier.height(16.dp))
134
    Text(what, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center)
135
}
136
137
@Composable
138
private fun Approve(code: DeviceCode, onCancel: () -> Unit) {
139
    val uri = LocalUriHandler.current
140
    Text("Approve this code on the hub", style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center)
141
    Spacer(Modifier.height(16.dp))
142
    Card(
143
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
144
        modifier = Modifier.fillMaxWidth(),
145
    ) {
146
        Text(
147
            code.userCode,
148
            fontFamily = FontFamily.Monospace,
149
            fontSize = 34.sp,
150
            fontWeight = FontWeight.Bold,
151
            letterSpacing = 4.sp,
152
            textAlign = TextAlign.Center,
153
            modifier = Modifier
154
                .fillMaxWidth()
155
                .padding(vertical = 24.dp)
156
                .semantics { contentDescription = "Code ${code.userCode.toCharArray().joinToString(" ")}" },
157
        )
158
    }
159
    Spacer(Modifier.height(16.dp))
160
    Button(onClick = { uri.openUri(code.pageToOpen) }, modifier = Modifier.fillMaxWidth().height(50.dp)) {
161
        Icon(Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
162
        Spacer(Modifier.size(8.dp))
163
        Text("Open the hub to approve", fontSize = 16.sp)
164
    }
165
    Spacer(Modifier.height(12.dp))
166
    Text(
167
        "Or on a computer, go to ${code.verificationUri} and enter the code.",
168
        style = MaterialTheme.typography.bodyMedium,
169
        color = MaterialTheme.colorScheme.onSurfaceVariant,
170
        textAlign = TextAlign.Center,
171
    )
172
    Spacer(Modifier.height(24.dp))
173
    LinearProgressIndicator(Modifier.fillMaxWidth())
174
    Spacer(Modifier.height(8.dp))
175
    Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
176
        Text("Waiting for approval…", style = MaterialTheme.typography.bodyMedium)
177
        TextButton(onClick = onCancel) { Text("Cancel") }
178
    }
179
}