OXYGENHUB
# 0381bd8af55e oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / signin / SignInScreen.kt
7.6 KiB · last changed Screenshots from Compose previews, and what rendering them showed (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
            color = MaterialTheme.colorScheme.onSurface,
149
            fontFamily = FontFamily.Monospace,
150
            fontSize = 34.sp,
151
            fontWeight = FontWeight.Bold,
152
            letterSpacing = 4.sp,
153
            textAlign = TextAlign.Center,
154
            modifier = Modifier
155
                .fillMaxWidth()
156
                .padding(vertical = 24.dp)
157
                .semantics { contentDescription = "Code ${code.userCode.toCharArray().joinToString(" ")}" },
158
        )
159
    }
160
    Spacer(Modifier.height(16.dp))
161
    Button(onClick = { uri.openUri(code.pageToOpen) }, modifier = Modifier.fillMaxWidth().height(50.dp)) {
162
        Icon(Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
163
        Spacer(Modifier.size(8.dp))
164
        Text("Open the hub to approve", fontSize = 16.sp)
165
    }
166
    Spacer(Modifier.height(12.dp))
167
    Text(
168
        "Or on a computer, go to ${code.verificationUri} and enter the code.",
169
        style = MaterialTheme.typography.bodyMedium,
170
        color = MaterialTheme.colorScheme.onSurfaceVariant,
171
        textAlign = TextAlign.Center,
172
    )
173
    Spacer(Modifier.height(24.dp))
174
    LinearProgressIndicator(Modifier.fillMaxWidth())
175
    Spacer(Modifier.height(8.dp))
176
    Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
177
        Text("Waiting for approval…", style = MaterialTheme.typography.bodyMedium)
178
        TextButton(onClick = onCancel) { Text("Cancel") }
179
    }
180
}