Skip to content

Commit 59b51b9

Browse files
committed
feat(app): add CredentialLinkingDemoActivity to sample app
1 parent b175030 commit 59b51b9

4 files changed

Lines changed: 231 additions & 13 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
android:label="Custom Slots & Theming Demo"
5656
android:exported="false"
5757
android:theme="@style/Theme.FirebaseUIAndroid" />
58+
59+
<activity
60+
android:name=".CredentialLinkingDemoActivity"
61+
android:label="Credential Linking Demo"
62+
android:exported="false"
63+
android:theme="@style/Theme.FirebaseUIAndroid" />
5864
</application>
5965

6066
</manifest>
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package com.firebaseui.android.demo
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.enableEdgeToEdge
7+
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.material3.Button
15+
import androidx.compose.material3.Card
16+
import androidx.compose.material3.CardDefaults
17+
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.OutlinedButton
19+
import androidx.compose.material3.Surface
20+
import androidx.compose.material3.Text
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.text.style.TextAlign
25+
import androidx.compose.ui.unit.dp
26+
import com.firebase.ui.auth.AuthException
27+
import com.firebase.ui.auth.AuthState
28+
import com.firebase.ui.auth.FirebaseAuthUI
29+
import com.firebase.ui.auth.configuration.authUIConfiguration
30+
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider
31+
import com.firebase.ui.auth.ui.screens.AuthRoute
32+
import com.firebase.ui.auth.ui.screens.AuthSuccessUiContext
33+
import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen
34+
35+
class CredentialLinkingDemoActivity : ComponentActivity() {
36+
override fun onCreate(savedInstanceState: Bundle?) {
37+
super.onCreate(savedInstanceState)
38+
enableEdgeToEdge()
39+
40+
val authUI = FirebaseAuthUI.getInstance()
41+
42+
val configuration = authUIConfiguration {
43+
context = applicationContext
44+
isCredentialLinkingEnabled = true
45+
providers {
46+
provider(
47+
AuthProvider.Email(
48+
isNewAccountsAllowed = true,
49+
emailLinkActionCodeSettings = null,
50+
passwordValidationRules = emptyList(),
51+
)
52+
)
53+
provider(
54+
AuthProvider.Google(
55+
scopes = listOf("email"),
56+
serverClientId = "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
57+
)
58+
)
59+
provider(
60+
AuthProvider.Phone(
61+
defaultNumber = null,
62+
defaultCountryCode = null,
63+
allowedCountries = emptyList(),
64+
timeout = 120L,
65+
)
66+
)
67+
}
68+
}
69+
70+
setContent {
71+
MaterialTheme {
72+
Surface(
73+
modifier = Modifier.fillMaxSize(),
74+
color = MaterialTheme.colorScheme.background
75+
) {
76+
FirebaseAuthScreen(
77+
configuration = configuration,
78+
authUI = authUI,
79+
onSignInSuccess = {},
80+
onSignInFailure = { _: AuthException -> },
81+
onSignInCancelled = {},
82+
authenticatedContent = { state, uiContext ->
83+
CredentialLinkingAuthenticatedContent(state, uiContext)
84+
}
85+
)
86+
}
87+
}
88+
}
89+
}
90+
}
91+
92+
@Composable
93+
private fun CredentialLinkingAuthenticatedContent(
94+
state: AuthState,
95+
uiContext: AuthSuccessUiContext,
96+
) {
97+
when (state) {
98+
is AuthState.Success -> {
99+
val user = state.user
100+
Column(
101+
modifier = Modifier
102+
.fillMaxSize()
103+
.padding(24.dp),
104+
horizontalAlignment = Alignment.CenterHorizontally,
105+
verticalArrangement = Arrangement.Center
106+
) {
107+
Text(
108+
text = "Signed in",
109+
style = MaterialTheme.typography.headlineSmall,
110+
)
111+
Spacer(modifier = Modifier.height(16.dp))
112+
Card(
113+
modifier = Modifier.fillMaxWidth(),
114+
colors = CardDefaults.cardColors(
115+
containerColor = MaterialTheme.colorScheme.surfaceVariant
116+
)
117+
) {
118+
Column(
119+
modifier = Modifier.padding(16.dp),
120+
verticalArrangement = Arrangement.spacedBy(8.dp)
121+
) {
122+
Text("UID: ${user.uid}", style = MaterialTheme.typography.bodySmall)
123+
Text("Email: ${user.email ?: ""}")
124+
Text("Phone: ${user.phoneNumber ?: ""}")
125+
Text(
126+
"Providers: ${user.providerData.map { it.providerId }}",
127+
style = MaterialTheme.typography.bodySmall,
128+
textAlign = TextAlign.Start
129+
)
130+
}
131+
}
132+
Spacer(modifier = Modifier.height(24.dp))
133+
Button(
134+
modifier = Modifier.fillMaxWidth(),
135+
onClick = { uiContext.onNavigate(AuthRoute.MethodPicker) }
136+
) {
137+
Text("Add sign-in method")
138+
}
139+
Spacer(modifier = Modifier.height(8.dp))
140+
OutlinedButton(
141+
modifier = Modifier.fillMaxWidth(),
142+
onClick = uiContext.onSignOut
143+
) {
144+
Text("Sign out")
145+
}
146+
}
147+
}
148+
149+
is AuthState.RequiresEmailVerification -> {
150+
Column(
151+
modifier = Modifier
152+
.fillMaxSize()
153+
.padding(24.dp),
154+
horizontalAlignment = Alignment.CenterHorizontally,
155+
verticalArrangement = Arrangement.Center
156+
) {
157+
Text(
158+
text = "Verify your email",
159+
style = MaterialTheme.typography.headlineSmall,
160+
)
161+
Spacer(modifier = Modifier.height(8.dp))
162+
Text(
163+
text = "A verification link was sent to ${state.email}. Once verified, tap the button below.",
164+
textAlign = TextAlign.Center,
165+
color = MaterialTheme.colorScheme.onSurfaceVariant
166+
)
167+
Spacer(modifier = Modifier.height(24.dp))
168+
Button(
169+
modifier = Modifier.fillMaxWidth(),
170+
onClick = uiContext.onReloadUser
171+
) {
172+
Text("I've verified my email")
173+
}
174+
Spacer(modifier = Modifier.height(8.dp))
175+
OutlinedButton(
176+
modifier = Modifier.fillMaxWidth(),
177+
onClick = uiContext.onSignOut
178+
) {
179+
Text("Sign out")
180+
}
181+
}
182+
}
183+
184+
else -> {}
185+
}
186+
}

app/src/main/java/com/firebaseui/android/demo/MainActivity.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import com.google.firebase.FirebaseApp
3636
*/
3737
class MainActivity : ComponentActivity() {
3838
companion object {
39-
private const val USE_AUTH_EMULATOR = false
39+
private const val USE_AUTH_EMULATOR = true
4040
private const val AUTH_EMULATOR_HOST = "10.0.2.2"
4141
private const val AUTH_EMULATOR_PORT = 9099
4242
}
@@ -94,6 +94,9 @@ class MainActivity : ComponentActivity() {
9494
onCustomSlotsClick = {
9595
startActivity(Intent(this, CustomSlotsThemingDemoActivity::class.java))
9696
},
97+
onCredentialLinkingClick = {
98+
startActivity(Intent(this, CredentialLinkingDemoActivity::class.java))
99+
},
97100
isEmulatorMode = USE_AUTH_EMULATOR
98101
)
99102
}
@@ -107,6 +110,7 @@ fun ChooserScreen(
107110
onHighLevelApiClick: () -> Unit,
108111
onLowLevelApiClick: () -> Unit,
109112
onCustomSlotsClick: () -> Unit,
113+
onCredentialLinkingClick: () -> Unit = {},
110114
isEmulatorMode: Boolean = false
111115
) {
112116
val scrollState = rememberScrollState()
@@ -272,6 +276,32 @@ fun ChooserScreen(
272276
}
273277
}
274278

279+
// Credential Linking Card
280+
Card(
281+
modifier = Modifier.fillMaxWidth(),
282+
onClick = onCredentialLinkingClick
283+
) {
284+
Column(
285+
modifier = Modifier.padding(20.dp),
286+
verticalArrangement = Arrangement.spacedBy(12.dp)
287+
) {
288+
Text(
289+
text = "🔗 Credential Linking",
290+
style = MaterialTheme.typography.titleLarge,
291+
color = MaterialTheme.colorScheme.primary
292+
)
293+
Text(
294+
text = "isCredentialLinkingEnabled",
295+
style = MaterialTheme.typography.titleMedium
296+
)
297+
Text(
298+
text = "Sign in with one provider, then add another to the same account without losing your UID.",
299+
style = MaterialTheme.typography.bodyMedium,
300+
color = MaterialTheme.colorScheme.onSurfaceVariant
301+
)
302+
}
303+
}
304+
275305
Spacer(modifier = Modifier.height(16.dp))
276306

277307
// Info card

auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import com.google.firebase.auth.AuthCredential
8282
import com.google.firebase.auth.AuthResult
8383
import com.google.firebase.auth.MultiFactorResolver
8484
import kotlinx.coroutines.launch
85+
import kotlinx.coroutines.tasks.await
8586

8687
/**
8788
* High-level authentication screen that wires together provider selection, individual provider
@@ -383,27 +384,22 @@ fun FirebaseAuthScreen(
383384
coroutineScope.launch {
384385
try {
385386
// Reload user to get fresh data from server
386-
authUI.getCurrentUser()?.reload()
387-
authUI.getCurrentUser()?.getIdToken(true)
388-
389-
// Check the user's email verification status after reload
390-
val user = authUI.getCurrentUser()
391-
if (user != null) {
392-
// If email is now verified, transition to Success state
393-
if (user.isEmailVerified) {
387+
authUI.getCurrentUser()?.let {
388+
it.reload().await()
389+
it.getIdToken(true).await()
390+
if (it.isEmailVerified) {
394391
authUI.updateAuthState(
395392
AuthState.Success(
396393
result = null,
397-
user = user,
394+
user = it,
398395
isNewUser = false
399396
)
400397
)
401398
} else {
402-
// Email still not verified, keep showing verification screen
403399
authUI.updateAuthState(
404400
AuthState.RequiresEmailVerification(
405-
user = user,
406-
email = user.email ?: ""
401+
user = it,
402+
email = it.email ?: ""
407403
)
408404
)
409405
}

0 commit comments

Comments
 (0)