Skip to content

Commit a2d2bc9

Browse files
committed
sample(auth): add MFA enrollment/challenge, reauth, and authenticated-state UI to full customization demo
1 parent 495268a commit a2d2bc9

11 files changed

Lines changed: 1296 additions & 0 deletions

File tree

app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,26 @@ import androidx.compose.ui.Modifier
1616
import androidx.compose.ui.layout.ContentScale
1717
import androidx.compose.ui.platform.LocalContext
1818
import androidx.compose.ui.res.painterResource
19+
import androidx.lifecycle.lifecycleScope
1920
import com.firebase.ui.auth.AuthException
2021
import com.firebase.ui.auth.FirebaseAuthUI
2122
import com.firebase.ui.auth.configuration.AuthUIConfiguration
23+
import com.firebase.ui.auth.configuration.MfaConfiguration
24+
import com.firebase.ui.auth.configuration.MfaFactor
2225
import com.firebase.ui.auth.configuration.authUIConfiguration
2326
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider
2427
import com.firebase.ui.auth.configuration.theme.AuthUIAsset
2528
import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen
2629
import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen
2730
import com.firebaseui.android.demo.R
2831
import com.firebaseui.android.demo.auth.fullcustomization.screens.AuthMethodPickerUI
32+
import com.firebaseui.android.demo.auth.fullcustomization.screens.AuthenticatedUI
33+
import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.MfaChallengeUI
34+
import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.MfaEnrollmentUI
2935
import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.PhoneSignInUI
36+
import com.firebaseui.android.demo.auth.fullcustomization.screens.reauth.ReauthUI
3037
import com.firebaseui.android.demo.auth.fullcustomization.theme.FullCustomizationTheme
38+
import kotlinx.coroutines.launch
3139

3240
class FullCustomizationDemoActivity : ComponentActivity() {
3341
override fun onCreate(savedInstanceState: Bundle?) {
@@ -88,6 +96,11 @@ class FullCustomizationDemoActivity : ComponentActivity() {
8896
onSignInCancelled = {
8997
Log.d("FullCustomizationDemo", "Auth cancelled")
9098
},
99+
mfaConfiguration = MfaConfiguration(
100+
allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp),
101+
requireEnrollment = false,
102+
enableRecoveryCodes = true,
103+
),
91104
customMethodPickerLayout = { providers, onProviderSelected ->
92105
MainUI(
93106
authUI = authUI,
@@ -97,6 +110,30 @@ class FullCustomizationDemoActivity : ComponentActivity() {
97110
)
98111
},
99112
phoneContent = { state -> PhoneSignInUI(state) },
113+
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
114+
mfaChallengeContent = { state -> MfaChallengeUI(state) },
115+
reauthContent = { reauthState, onDismiss ->
116+
ReauthUI(
117+
state = reauthState,
118+
onDismiss = onDismiss,
119+
onVerified = {
120+
val retry = reauthState.retryOperation
121+
// Dismiss first: onDismiss resets the auth state to Idle, so
122+
// running the retry afterwards keeps the Success it reports
123+
// from being clobbered. Neither call suspends, so the
124+
// composable's cancellation can't interleave between them —
125+
// and the retry itself runs on lifecycleScope, which outlives
126+
// this UI.
127+
onDismiss()
128+
retry?.let {
129+
lifecycleScope.launch { it(applicationContext) }
130+
}
131+
},
132+
)
133+
},
134+
authenticatedContent = { state, uiContext ->
135+
AuthenticatedUI(state = state, uiContext = uiContext)
136+
},
100137
)
101138
}
102139
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.firebaseui.android.demo.auth.fullcustomization.common
2+
3+
import androidx.annotation.DrawableRes
4+
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.BoxWithConstraints
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.ColumnScope
10+
import androidx.compose.foundation.layout.Spacer
11+
import androidx.compose.foundation.layout.fillMaxSize
12+
import androidx.compose.foundation.layout.fillMaxWidth
13+
import androidx.compose.foundation.layout.height
14+
import androidx.compose.foundation.layout.heightIn
15+
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.safeDrawingPadding
17+
import androidx.compose.foundation.layout.size
18+
import androidx.compose.foundation.rememberScrollState
19+
import androidx.compose.foundation.verticalScroll
20+
import androidx.compose.material3.MaterialTheme
21+
import androidx.compose.material3.Surface
22+
import androidx.compose.material3.Text
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.layout.ContentScale
26+
import androidx.compose.ui.res.painterResource
27+
import androidx.compose.ui.semantics.contentDescription
28+
import androidx.compose.ui.semantics.semantics
29+
import androidx.compose.ui.unit.dp
30+
import com.firebaseui.android.demo.R
31+
32+
/**
33+
* The page frame shared by the MFA and reauthentication screens: mascot, headline, a single
34+
* elevated card, and bottom-anchored actions.
35+
*
36+
* The email and phone steps predate this and inline the same structure themselves.
37+
*
38+
* verticalScroll measures content with infinite max height, and Column distributes weights
39+
* against the MIN height when max is infinite (RowColumnMeasurePolicy.kt) — so
40+
* heightIn(min = viewport) makes the weighted spacers expand (centering content, anchoring the
41+
* actions to the bottom) when everything fits, and collapse to zero (plain scrolling) when it
42+
* doesn't.
43+
*/
44+
@Composable
45+
fun AuthPage(
46+
@DrawableRes mascot: Int,
47+
mascotDescription: String,
48+
title: String,
49+
cardContentDescription: String,
50+
actions: @Composable ColumnScope.() -> Unit,
51+
card: @Composable ColumnScope.() -> Unit,
52+
) {
53+
Box(modifier = Modifier.fillMaxSize()) {
54+
// Full-bleed, and deliberately outside the safeDrawingPadding below so it runs edge to
55+
// edge under the system bars — same as MainUI and PhoneSignInUI do for their slots.
56+
Image(
57+
painter = painterResource(id = R.drawable.custom_background),
58+
contentDescription = null,
59+
contentScale = ContentScale.Crop,
60+
modifier = Modifier.fillMaxSize(),
61+
)
62+
63+
BoxWithConstraints(
64+
modifier = Modifier
65+
.fillMaxSize()
66+
.safeDrawingPadding(),
67+
) {
68+
Column(
69+
modifier = Modifier
70+
.fillMaxWidth()
71+
.verticalScroll(rememberScrollState())
72+
.heightIn(min = maxHeight)
73+
.padding(horizontal = 40.dp, vertical = 24.dp),
74+
) {
75+
Spacer(modifier = Modifier.weight(1f))
76+
77+
Column(modifier = Modifier.fillMaxWidth()) {
78+
Image(
79+
painter = painterResource(id = mascot),
80+
contentDescription = mascotDescription,
81+
modifier = Modifier.size(72.dp),
82+
)
83+
Spacer(modifier = Modifier.height(8.dp))
84+
Text(
85+
text = title,
86+
style = MaterialTheme.typography.headlineSmall,
87+
color = MaterialTheme.colorScheme.primary,
88+
)
89+
Spacer(modifier = Modifier.height(24.dp))
90+
91+
HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) {
92+
Surface(
93+
modifier = Modifier
94+
.fillMaxWidth()
95+
.semantics { contentDescription = cardContentDescription },
96+
color = MaterialTheme.colorScheme.surface,
97+
shape = AuthFieldShape,
98+
) {
99+
Column(
100+
modifier = Modifier.padding(16.dp),
101+
verticalArrangement = Arrangement.spacedBy(16.dp),
102+
content = card,
103+
)
104+
}
105+
}
106+
}
107+
108+
Spacer(modifier = Modifier.weight(1f))
109+
Spacer(modifier = Modifier.height(24.dp))
110+
111+
Column(modifier = Modifier.fillMaxWidth(), content = actions)
112+
}
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)