1
+ package com.firebase.uidemo.auth.compose
2
+
3
+ import android.content.Context
4
+ import androidx.compose.foundation.layout.*
5
+ import androidx.compose.foundation.shape.CircleShape
6
+ import androidx.compose.material.icons.Icons
7
+ import androidx.compose.material.icons.filled.Delete
8
+ import androidx.compose.material.icons.filled.ExitToApp
9
+ import androidx.compose.material3.*
10
+ import androidx.compose.runtime.*
11
+ import androidx.compose.ui.Alignment
12
+ import androidx.compose.ui.Modifier
13
+ import androidx.compose.ui.draw.clip
14
+ import androidx.compose.ui.platform.LocalContext
15
+ import androidx.compose.ui.res.stringResource
16
+ import androidx.compose.ui.text.style.TextOverflow
17
+ import androidx.compose.ui.unit.dp
18
+ import com.bumptech.glide.integration.compose.GlideImage
19
+ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
20
+ import com.firebase.ui.auth.AuthUI
21
+ import com.firebase.ui.auth.IdpResponse
22
+ import com.firebase.uidemo.R
23
+ import com.google.firebase.auth.*
24
+ import kotlinx.coroutines.launch
25
+
26
+ @OptIn(ExperimentalMaterial3Api ::class )
27
+ @Composable
28
+ fun SignedInScreen (
29
+ idpResponse : IdpResponse ? ,
30
+ onSignedOut : () -> Unit
31
+ ) {
32
+ val context = LocalContext .current
33
+ val scope = rememberCoroutineScope()
34
+ val snackbar = remember { SnackbarHostState () }
35
+
36
+ val firebaseUser by produceState(initialValue = FirebaseAuth .getInstance().currentUser) {
37
+ val listener = FirebaseAuth .AuthStateListener { auth -> value = auth.currentUser }
38
+ FirebaseAuth .getInstance().addAuthStateListener(listener)
39
+ awaitDispose { FirebaseAuth .getInstance().removeAuthStateListener(listener) }
40
+ }
41
+ if (firebaseUser == null ) {
42
+ onSignedOut(); return
43
+ }
44
+
45
+ var askDelete by remember { mutableStateOf(false ) }
46
+
47
+ Scaffold (
48
+ snackbarHost = { SnackbarHost (snackbar) },
49
+ topBar = { CenterAlignedTopAppBar (title = { Text (" Profile" ) }) },
50
+ floatingActionButton = {
51
+ FloatingActionButton (onClick = { askDelete = true }) {
52
+ Icon (Icons .Default .Delete , contentDescription = null )
53
+ }
54
+ }
55
+ ) { padding ->
56
+ ProfileContent (
57
+ modifier = Modifier .padding(padding),
58
+ user = firebaseUser!! ,
59
+ isNewUser = idpResponse?.isNewUser ? : false ,
60
+ idpToken = idpResponse?.idpToken,
61
+ idpSecret = idpResponse?.idpSecret,
62
+ onSignOut = {
63
+ AuthUI .getInstance().signOut(context).addOnCompleteListener { task ->
64
+ if (task.isSuccessful) onSignedOut()
65
+ else scope.launch {
66
+ snackbar.showSnackbar(
67
+ context.getString(R .string.sign_out_failed)
68
+ )
69
+ }
70
+ }
71
+ }
72
+ )
73
+ }
74
+
75
+ if (askDelete) {
76
+ AlertDialog (
77
+ onDismissRequest = { askDelete = false },
78
+ confirmButton = {
79
+ TextButton (onClick = {
80
+ askDelete = false
81
+ AuthUI .getInstance().delete(context).addOnCompleteListener { task ->
82
+ if (task.isSuccessful) onSignedOut()
83
+ else scope.launch {
84
+ snackbar.showSnackbar(
85
+ context.getString(R .string.delete_account_failed)
86
+ )
87
+ }
88
+ }
89
+ }) { Text (" Yes, nuke it!" ) }
90
+ },
91
+ dismissButton = { TextButton (onClick = { askDelete = false }) { Text (" No" ) } },
92
+ title = { Text (" Delete account" ) },
93
+ text = { Text (" Are you sure you want to delete this account?" ) }
94
+ )
95
+ }
96
+ }
97
+
98
+ /* ---------------- profile details ---------------- */
99
+
100
+ @OptIn(ExperimentalGlideComposeApi ::class )
101
+ @Composable
102
+ private fun ProfileContent (
103
+ modifier : Modifier = Modifier ,
104
+ user : FirebaseUser ,
105
+ isNewUser : Boolean ,
106
+ idpToken : String? ,
107
+ idpSecret : String? ,
108
+ onSignOut : () -> Unit
109
+ ) {
110
+ val ctx = LocalContext .current
111
+ val providers = remember(user) { user.enabledProviderNames(ctx) }
112
+
113
+ Column (
114
+ modifier = modifier
115
+ .fillMaxSize()
116
+ .padding(24 .dp),
117
+ horizontalAlignment = Alignment .CenterHorizontally
118
+ ) {
119
+ user.photoUrl?.let { url ->
120
+ GlideImage (
121
+ model = url,
122
+ contentDescription = null ,
123
+ modifier = Modifier
124
+ .size(96 .dp)
125
+ .clip(CircleShape )
126
+ )
127
+ Spacer (Modifier .height(16 .dp))
128
+ }
129
+
130
+ InfoRow (label = " Email" , value = user.email)
131
+ InfoRow (label = " Phone" , value = user.phoneNumber)
132
+ InfoRow (label = " Display name" , value = user.displayName)
133
+
134
+ if (isNewUser) {
135
+ Text (
136
+ " New user" ,
137
+ style = MaterialTheme .typography.labelMedium,
138
+ color = MaterialTheme .colorScheme.primary,
139
+ modifier = Modifier .padding(top = 8 .dp)
140
+ )
141
+ }
142
+
143
+ Spacer (Modifier .height(12 .dp))
144
+
145
+ Text (
146
+ text = stringResource(R .string.used_providers, providers),
147
+ style = MaterialTheme .typography.bodySmall
148
+ )
149
+
150
+ if (idpToken != null ) {
151
+ Spacer (Modifier .height(12 .dp))
152
+ TokenBlock (label = " IDP Token" , value = idpToken)
153
+ }
154
+ if (idpSecret != null ) {
155
+ Spacer (Modifier .height(8 .dp))
156
+ TokenBlock (label = " IDP Secret" , value = idpSecret)
157
+ }
158
+
159
+ Spacer (Modifier .weight(1f ))
160
+
161
+ Button (
162
+ onClick = onSignOut,
163
+ modifier = Modifier .fillMaxWidth()
164
+ ) {
165
+ Icon (Icons .Default .ExitToApp , contentDescription = null )
166
+ Spacer (Modifier .width(8 .dp))
167
+ Text (stringResource(R .string.sign_out))
168
+ }
169
+ }
170
+ }
171
+
172
+ @Composable
173
+ private fun InfoRow (label : String , value : String? ) {
174
+ Row (
175
+ modifier = Modifier .fillMaxWidth(),
176
+ horizontalArrangement = Arrangement .SpaceBetween
177
+ ) {
178
+ Text (label)
179
+ Text (
180
+ text = value.takeUnless { it.isNullOrBlank() } ? : " —" ,
181
+ overflow = TextOverflow .Ellipsis ,
182
+ maxLines = 1
183
+ )
184
+ }
185
+ }
186
+
187
+ @Composable
188
+ private fun TokenBlock (label : String , value : String ) {
189
+ Column (Modifier .fillMaxWidth()) {
190
+ Text (label, style = MaterialTheme .typography.labelSmall)
191
+ Text (value, style = MaterialTheme .typography.bodySmall, overflow = TextOverflow .Ellipsis )
192
+ }
193
+ }
194
+
195
+ private fun FirebaseUser.enabledProviderNames (ctx : Context ): String {
196
+ if (providerData.isEmpty()) return ctx.getString(R .string.providers_anonymous)
197
+
198
+ val names = providerData.mapNotNull { info ->
199
+ when (info.providerId) {
200
+ GoogleAuthProvider .PROVIDER_ID -> ctx.getString(R .string.providers_google)
201
+ FacebookAuthProvider .PROVIDER_ID -> ctx.getString(R .string.providers_facebook)
202
+ TwitterAuthProvider .PROVIDER_ID -> ctx.getString(R .string.providers_twitter)
203
+ EmailAuthProvider .PROVIDER_ID -> ctx.getString(R .string.providers_email)
204
+ PhoneAuthProvider .PROVIDER_ID -> ctx.getString(R .string.providers_phone)
205
+ AuthUI .EMAIL_LINK_PROVIDER -> ctx.getString(R .string.providers_email_link)
206
+ FirebaseAuthProvider .PROVIDER_ID -> null // ignore
207
+ else -> info.providerId
208
+ }
209
+ }
210
+ return names.joinToString()
211
+ }
0 commit comments