diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a6d5269d33..ca5a5bb3b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,8 +31,8 @@ jobs: java-version: '21' distribution: 'temurin' - # lintAll gates :proguard-tests, which applies the google-services plugin and - # will not configure without this file. Mirrors what scripts/build.sh copies. + # lintAll gates :app and :proguard-tests, both of which apply the google-services + # plugin and will not configure without this file. Mirrors what build.sh copies. - name: Copy google-services.json run: | cp library/google-services.json app/google-services.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de3ba2f5ba..1a98334baf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,7 @@ This will: - Copy the necessary `google-services.json` files - Download all dependencies - Build all modules +- Run the R8/ProGuard gate - Run checkstyle - Run unit tests @@ -145,6 +146,7 @@ commands can be run locally to highlight any issues before committing your code: This script runs: - `./gradlew clean` - `./gradlew assembleDebug` - Build all modules +- `./gradlew proguard-tests:build` - Check the libraries' consumer ProGuard rules against R8 - `./gradlew checkstyle` - Run code style checks - `./gradlew testDebugUnitTest -x :e2eTest:testDebugUnitTest` - Run unit tests diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c7870c5001..ba8fded8fc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,8 +6,7 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") id("com.google.gms.google-services") id("kotlin-kapt") - // The slot demos host the auth screens on their own Navigation 3 back stacks, and a - // rememberNavBackStack key has to be @Serializable to survive process death. + // Nav3 back-stack keys must be @Serializable to survive process death. alias(libs.plugins.kotlin.serialization) } @@ -39,6 +38,26 @@ android { } } } + + lint { + // Module specific + disable += mutableSetOf( + // Reads the root wrapper, but only the application module analyses it, so it + // belongs here rather than in the shared policy. For reproducible builds. + "AndroidGradlePluginVersion", + // The demos log their auth callbacks unconditionally on purpose — watching + // logcat is how you see one fire. Unlike :auth's, none of these log user data. + "LogConditional", + // Glide's KSP processor does not generate GlideApp, which the storage demo and + // storage/README.md are both written around. Migration tracked separately. + "KaptUsageInsteadOfKsp", + // A themed icon needs a flat silhouette drawn for the purpose. The only + // candidate here is ic_launcher_foreground, whose opaque region is a solid + // plate, so it tints to a featureless block — worse than no monochrome layer. + "MonochromeLauncherIcon" + ) + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -71,6 +90,7 @@ dependencies { implementation(libs.compose.ui.graphics) implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.material3) + implementation(libs.compose.material.icons.extended) // Facebook implementation(libs.facebook.login) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 77abc57765..9e310ab7cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,6 @@ android:usesCleartextTraffic="true"> @@ -22,7 +21,13 @@ - + + @@ -88,6 +93,12 @@ android:exported="false" android:theme="@style/Theme.FirebaseUIAndroid" /> + + + FullCustomizationDemoActivity::class.java + // Untagged links predate this routing, so they belong to the demo that had them. + else -> HighLevelApiDemoActivity::class.java + } + val demoIntent = Intent(this, target).apply { pendingEmailLink?.let { link -> putExtra(EmailLinkConstants.EXTRA_EMAIL_LINK, link) pendingEmailLink = null @@ -96,7 +104,7 @@ class MainActivity : ComponentActivity() { } if (savedInstanceState == null && !pendingEmailLink.isNullOrEmpty()) { - launchHighLevelDemo() + launchDemoForEmailLink() finish() return } diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/CustomMethodPickerDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/CustomMethodPickerDemoActivity.kt index 5228927d9f..dbe3a27271 100644 --- a/app/src/main/java/com/firebaseui/android/demo/auth/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/auth/CustomMethodPickerDemoActivity.kt @@ -142,6 +142,7 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { Log.d("CustomMethodPickerDemo", "Auth cancelled") }, customMethodPickerLayout = { providers, onProviderSelected -> + // Owns the whole screen now, so the terms checkbox is inline here. SpotlightMethodPicker( providers = providers, onProviderSelected = onProviderSelected, @@ -181,6 +182,7 @@ fun SpotlightMethodPicker( val anonymous = groups["anonymous"]?.firstOrNull() LazyColumn( + // Owns the whole screen, so it handles its own insets. modifier = Modifier .fillMaxSize() .safeDrawingPadding(), @@ -298,7 +300,7 @@ fun SpotlightMethodPicker( } @Composable -private fun ProviderIconButton( +fun ProviderIconButton( style: AuthUITheme.ProviderStyle, contentDescription: String, onClick: () -> Unit, @@ -335,12 +337,12 @@ private fun ProviderIconButton( } @Composable -private fun AuthUIAsset.asPainter(): Painter = when (this) { +fun AuthUIAsset.asPainter(): Painter = when (this) { is AuthUIAsset.Resource -> painterResource(resId) is AuthUIAsset.Vector -> rememberVectorPainter(image) } -private fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle = when (provider) { +fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle = when (provider) { is AuthProvider.Facebook -> ProviderStyleDefaults.Facebook is AuthProvider.Twitter -> ProviderStyleDefaults.Twitter is AuthProvider.Github -> ProviderStyleDefaults.Github diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt index df069091f9..d270beac55 100644 --- a/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.firebaseui.android.demo.auth.fullcustomization.FullCustomizationDemoActivity class CustomSlotsThemingDemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -46,6 +47,9 @@ class CustomSlotsThemingDemoActivity : ComponentActivity() { }, onCustomMethodPickerClick = { startActivity(Intent(this, CustomMethodPickerDemoActivity::class.java)) + }, + onFullCustomizationClick = { + startActivity(Intent(this, FullCustomizationDemoActivity::class.java)) } ) } @@ -60,6 +64,7 @@ fun CustomSlotsDemoChooser( onPhoneAuthSlotClick: () -> Unit, onShapeCustomizationClick: () -> Unit, onCustomMethodPickerClick: () -> Unit, + onFullCustomizationClick: () -> Unit, ) { Column( modifier = Modifier @@ -106,6 +111,12 @@ fun CustomSlotsDemoChooser( description = "Replace the default provider list with a custom layout, and swap the 'By continuing...' footer with a checkbox using customMethodPickerLayout and customMethodPickerTermsConfiguration on FirebaseAuthScreen.", onClick = onCustomMethodPickerClick ) + + DemoCard( + title = "Full Customization", + description = "customMethodPickerLayout renders as the entire screen, so this layers a full-bleed background image and scrim behind the custom method picker.", + onClick = onFullCustomizationClick + ) } } diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt index aa8216c53f..589bc54d9e 100644 --- a/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt @@ -67,9 +67,15 @@ import com.firebase.ui.auth.util.EmailLinkConstants import com.firebase.ui.auth.util.displayIdentifier import com.firebase.ui.auth.util.getDisplayEmail import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.utils.EMAIL_LINK_ORIGIN_PARAM import com.google.firebase.auth.actionCodeSettings class HighLevelApiDemoActivity : ComponentActivity() { + companion object { + /** Marks this demo's email links so MainActivity can route the return trip back here. */ + const val EMAIL_LINK_ORIGIN = "highlevel" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -131,7 +137,9 @@ class HighLevelApiDemoActivity : ComponentActivity() { isEmailLinkForceSameDeviceEnabled = false, isEmailLinkSignInEnabled = true, emailLinkActionCodeSettings = actionCodeSettings { - url = "https://flutterfire-e2e-tests.firebaseapp.com" + // This tag is what MainActivity routes the return trip on. + url = "https://flutterfire-e2e-tests.firebaseapp.com" + + "?$EMAIL_LINK_ORIGIN_PARAM=$EMAIL_LINK_ORIGIN" handleCodeInApp = true setAndroidPackageName( "com.firebaseui.android.demo", diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt new file mode 100644 index 0000000000..d3cccd059b --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt @@ -0,0 +1,182 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.configuration.MfaConfiguration +import com.firebase.ui.auth.configuration.MfaFactor +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.utils.EMAIL_LINK_ORIGIN_PARAM +import com.firebaseui.android.demo.auth.fullcustomization.screens.AuthMethodPickerUI +import com.firebaseui.android.demo.auth.fullcustomization.screens.AuthenticatedUI +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.EmailAuthUI +import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.MfaChallengeUI +import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.MfaEnrollmentUI +import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.PhoneSignInUI +import com.firebaseui.android.demo.auth.fullcustomization.screens.reauth.ReauthUI +import com.firebaseui.android.demo.auth.fullcustomization.theme.FullCustomizationTheme +import com.firebase.ui.auth.util.EmailLinkConstants +import com.google.firebase.auth.actionCodeSettings + +class FullCustomizationDemoActivity : ComponentActivity() { + companion object { + /** Marks this demo's email links so MainActivity can route the return trip back here. */ + const val EMAIL_LINK_ORIGIN = "fullcustomization" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + // MainActivity owns the deep link and hands it on once it works out which demo sent it. + val emailLink = intent.getStringExtra(EmailLinkConstants.EXTRA_EMAIL_LINK) + val configuration = authUIConfiguration { + context = applicationContext + logo = AuthUIAsset.Resource(R.drawable.firebase_auth) + tosUrl = "https://policies.google.com/terms" + privacyPolicyUrl = "https://policies.google.com/privacy" + providers { + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + ) + ) + provider(AuthProvider.Apple(customParameters = emptyMap(), locale = null)) + provider(AuthProvider.Facebook()) + provider(AuthProvider.Twitter(customParameters = emptyMap())) + provider(AuthProvider.Github(customParameters = emptyMap())) + provider(AuthProvider.Microsoft(tenant = null, customParameters = emptyMap())) + provider(AuthProvider.Yahoo(customParameters = emptyMap())) + provider( + AuthProvider.Email( + isEmailLinkSignInEnabled = true, + emailLinkActionCodeSettings = actionCodeSettings { + // This tag is what MainActivity routes the return trip on. + url = "https://flutterfire-e2e-tests.firebaseapp.com" + + "?$EMAIL_LINK_ORIGIN_PARAM=$EMAIL_LINK_ORIGIN" + handleCodeInApp = true + setAndroidPackageName( + "com.firebaseui.android.demo", + true, + null + ) + }, + passwordValidationRules = emptyList() + ) + ) + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null + ) + ) + provider(AuthProvider.Anonymous) + } + } + + setContent { + FullCustomizationTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + emailLink = emailLink, + onSignInSuccess = { result -> + Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}") + }, + onSignInFailure = { exception: AuthException -> + Log.e("FullCustomizationDemo", "Auth failed", exception) + }, + onSignInCancelled = { + Log.d("FullCustomizationDemo", "Auth cancelled") + }, + mfaConfiguration = MfaConfiguration( + allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp), + requireEnrollment = false, + ), + customMethodPickerLayout = { providers, onProviderSelected -> + MainUI( + authUI = authUI, + configuration = configuration, + providers = providers, + onProviderSelected = onProviderSelected, + ) + }, + // Covers the email flows the library navigates to itself. + emailContent = { state -> EmailAuthUI(state) }, + phoneContent = { state -> PhoneSignInUI(state) }, + mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) }, + mfaChallengeContent = { state -> MfaChallengeUI(state) }, + reauthContent = { state -> ReauthUI(state) }, + authenticatedContent = { state, uiContext -> + AuthenticatedUI(state = state, uiContext = uiContext) + }, + ) + } + } + } + } +} + +@Composable +private fun MainUI( + authUI: FirebaseAuthUI, + configuration: AuthUIConfiguration, + providers: List, + onProviderSelected: (AuthProvider) -> Unit, +) { + val context = LocalContext.current + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(id = R.drawable.custom_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + Column(modifier = Modifier.fillMaxSize()) { + // AuthMethodPickerUI builds its own EmailAuthScreen per step, so none is needed here. + AuthMethodPickerUI( + context = context, + configuration = configuration, + authUI = authUI, + otherProviders = providers.filterNot { it is AuthProvider.Email }, + onProviderSelected = onProviderSelected, + onSuccess = { result -> + Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}") + }, + onError = { exception -> + Log.e("FullCustomizationDemo", "Auth failed", exception) + }, + onCancel = { + Log.d("FullCustomizationDemo", "Auth cancelled") + }, + ) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthPage.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthPage.kt new file mode 100644 index 0000000000..8bb588dfdb --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthPage.kt @@ -0,0 +1,114 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.firebaseui.android.demo.R + +/** + * The page frame shared by the MFA and reauthentication screens: mascot, headline, a single + * elevated card, and bottom-anchored actions. + * + * The email and phone steps predate this and inline the same structure themselves. + * + * verticalScroll measures content with infinite max height, and Column distributes weights + * against the MIN height when max is infinite (RowColumnMeasurePolicy.kt) — so + * heightIn(min = viewport) makes the weighted spacers expand (centering content, anchoring the + * actions to the bottom) when everything fits, and collapse to zero (plain scrolling) when it + * doesn't. + */ +@Composable +fun AuthPage( + @DrawableRes mascot: Int, + mascotDescription: String, + title: String, + cardContentDescription: String, + actions: @Composable ColumnScope.() -> Unit, + card: @Composable ColumnScope.() -> Unit, +) { + Box(modifier = Modifier.fillMaxSize()) { + // Outside safeDrawingPadding so it runs edge to edge under the system bars. + Image( + painter = painterResource(id = R.drawable.custom_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 40.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(id = mascot), + contentDescription = mascotDescription, + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = cardContentDescription }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + content = card, + ) + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + Column(modifier = Modifier.fillMaxWidth(), content = actions) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthTextFieldStyle.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthTextFieldStyle.kt new file mode 100644 index 0000000000..ca66ee7bc2 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthTextFieldStyle.kt @@ -0,0 +1,75 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.firebaseui.android.demo.R + +val AuthFieldShape = RoundedCornerShape(24.dp) + +@Composable +fun authTextFieldColors(): TextFieldColors = OutlinedTextFieldDefaults.colors( + unfocusedContainerColor = Color.White, + focusedContainerColor = Color.White, + disabledContainerColor = Color.White, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, + focusedBorderColor = MaterialTheme.colorScheme.secondary, +) + +@Composable +fun FullCustomizationTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String? = null, + placeholder: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + enabled: Boolean = true, + isError: Boolean = false, + supportingText: String? = null, + singleLine: Boolean = true, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + shape: Shape = AuthFieldShape, +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + label = label?.let { { Text(it) } }, + placeholder = placeholder?.let { { Text(it) } }, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + enabled = enabled, + isError = isError, + supportingText = supportingText?.let { { Text(it) } }, + singleLine = singleLine, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + shape = shape, + colors = authTextFieldColors(), + ) +} + +@Composable +fun EmailFieldIcon() { + Image( + painter = painterResource(R.drawable.email_at_sign), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/CtaButton.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/CtaButton.kt new file mode 100644 index 0000000000..edb251c936 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/CtaButton.kt @@ -0,0 +1,55 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.firebaseui.android.demo.auth.fullcustomization.theme.ButtonShape + +private val CtaShadowColor = Color(0xFF5D0B47) + +@Composable +fun CtaButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isLoading: Boolean = false, + colors: ButtonColors = ButtonDefaults.buttonColors(), +) { + HardOffsetShadow( + shape = ButtonShape, + offsetX = 2.dp, + offsetY = 4.dp, + color = if (enabled) CtaShadowColor else Color.Transparent, + modifier = modifier.fillMaxWidth(), + ) { + Button( + onClick = onClick, + enabled = enabled, + shape = ButtonShape, + colors = colors, + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 10.dp), + modifier = Modifier + .fillMaxWidth() + .height(80.dp), + ) { + if (isLoading) { + // M3 default (primary) reads on the disabled fill; LocalContentColor would wash it out. + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + Text(text = text, style = MaterialTheme.typography.titleMedium) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/HardOffsetShadow.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/HardOffsetShadow.kt new file mode 100644 index 0000000000..628bffafd2 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/HardOffsetShadow.kt @@ -0,0 +1,32 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun HardOffsetShadow( + shape: Shape, + modifier: Modifier = Modifier, + offsetX: Dp = 3.dp, + offsetY: Dp = 6.dp, + color: Color = MaterialTheme.colorScheme.primaryContainer, + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + Box( + modifier = Modifier + .matchParentSize() + .offset(x = offsetX, y = offsetY) + .background(color = color, shape = shape), + ) + content() + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/OtherSignInMethodsSheet.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/OtherSignInMethodsSheet.kt new file mode 100644 index 0000000000..c5919e1699 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/OtherSignInMethodsSheet.kt @@ -0,0 +1,74 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.ui.components.TermsAndPrivacyForm + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OtherSignInMethodsSheet( + otherProviders: List, + onProviderSelected: (AuthProvider) -> Unit, + onDismissRequest: () -> Unit, + tosUrl: String?, + ppUrl: String?, +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + containerColor = MaterialTheme.colorScheme.primaryContainer, + ) { + // Scrollable: nine providers plus the ToS footer overflow a sheet on shorter screens. + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 64.dp), + ) { + Text( + text = "Other sign in methods", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + .semantics { contentDescription = "Other sign-in methods sheet title" }, + ) + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth(), + ) { + otherProviders.forEach { provider -> + SheetProviderButton( + provider = provider, + onClick = { + onDismissRequest() + onProviderSelected(provider) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + TermsAndPrivacyForm(tosUrl = tosUrl, ppUrl = ppUrl) + Spacer(modifier = Modifier.height(24.dp)) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/SheetProviderButton.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/SheetProviderButton.kt new file mode 100644 index 0000000000..905774fe0e --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/SheetProviderButton.kt @@ -0,0 +1,123 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults +import com.firebaseui.android.demo.auth.fullcustomization.theme.ProviderButtonShape + +@Composable +fun SheetProviderButton( + provider: AuthProvider, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val label = providerSheetLabel(provider) + val style = when (provider) { + is AuthProvider.Google -> ProviderStyleDefaults.Google + is AuthProvider.Facebook -> ProviderStyleDefaults.Facebook + is AuthProvider.Twitter -> ProviderStyleDefaults.Twitter + is AuthProvider.Github -> ProviderStyleDefaults.Github + is AuthProvider.Microsoft -> ProviderStyleDefaults.Microsoft + is AuthProvider.Yahoo -> ProviderStyleDefaults.Yahoo + is AuthProvider.Apple -> ProviderStyleDefaults.Apple + is AuthProvider.Anonymous -> ProviderStyleDefaults.Anonymous + else -> ProviderStyleDefaults.Email + } + val backgroundColor = if (provider is AuthProvider.Phone) { + MaterialTheme.colorScheme.primary + } else { + style.backgroundColor + } + val contentColor = if (provider is AuthProvider.Google) Color.Black else style.contentColor + val hasWhiteBackground = backgroundColor == Color.White + + Button( + onClick = onClick, + shape = ProviderButtonShape, + colors = ButtonDefaults.buttonColors( + containerColor = backgroundColor, + contentColor = contentColor, + ), + border = if (hasWhiteBackground) BorderStroke(1.dp, Color.Black) else null, + contentPadding = PaddingValues(horizontal = 36.dp, vertical = 12.dp), + modifier = modifier, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + if (provider is AuthProvider.Phone) { + Icon( + imageVector = Icons.Default.Phone, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + } else { + style.icon?.let { icon -> + Image( + painter = icon.asPainter(), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + } + } + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = label, + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + style = MaterialTheme.typography.labelLarge, + ) + } + } +} + +private fun providerSheetLabel(provider: AuthProvider): String = when (provider) { + is AuthProvider.Google -> "Sign in with Google" + is AuthProvider.Facebook -> "Sign in with Facebook" + is AuthProvider.Twitter -> "Sign in with X" + is AuthProvider.Github -> "Sign in with GitHub" + is AuthProvider.Microsoft -> "Sign in with Microsoft" + is AuthProvider.Yahoo -> "Sign in with Yahoo" + is AuthProvider.Apple -> "Sign in with Apple" + is AuthProvider.Phone -> "Sign in with phone" + is AuthProvider.Anonymous -> "Continue as guest" + // Email only reaches this button during reauthentication; the sign-in sheet filters it out. + is AuthProvider.Email -> "Continue with email" + else -> "Continue" +} + +@Composable +private fun AuthUIAsset.asPainter() = when (this) { + is AuthUIAsset.Resource -> painterResource(resId) + is AuthUIAsset.Vector -> rememberVectorPainter(image) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthMethodPickerUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthMethodPickerUI.kt new file mode 100644 index 0000000000..9592a31203 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthMethodPickerUI.kt @@ -0,0 +1,207 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens + +import android.content.Context +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.scene.Scene +import androidx.navigation3.ui.NavDisplay +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.ui.screens.AuthRoute +import com.firebase.ui.auth.ui.screens.email.EmailAuthMode +import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen +import com.firebaseui.android.demo.auth.fullcustomization.common.OtherSignInMethodsSheet +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.EmailEntryStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.LoginStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.SignUpStep +import com.google.firebase.auth.AuthResult +import kotlinx.serialization.Serializable + +/** + * The demo's own pre-step, entered before any [AuthRoute.Email.Step]: type an address, then choose + * to sign in or create an account. Not part of the library's [AuthRoute] — that sealed hierarchy is + * closed outside the auth module — so this is a plain [NavKey] of the demo's own, sharing the same + * back stack as the library's per-mode destinations. + */ +@Serializable +private data object EmailEntryKey : NavKey + +private fun AuthRoute.Email.Step.toMode(): EmailAuthMode = when (this) { + is AuthRoute.Email.SignIn -> EmailAuthMode.SignIn + is AuthRoute.Email.SignUp -> EmailAuthMode.SignUp + is AuthRoute.Email.ResetPassword -> EmailAuthMode.ResetPassword + is AuthRoute.Email.EmailLinkSignIn -> EmailAuthMode.EmailLinkSignIn +} + +private fun stepFor(mode: EmailAuthMode, email: String?): AuthRoute.Email.Step = when (mode) { + EmailAuthMode.SignIn -> AuthRoute.Email.SignIn(email) + EmailAuthMode.SignUp -> AuthRoute.Email.SignUp(email) + EmailAuthMode.ResetPassword -> AuthRoute.Email.ResetPassword(email) + EmailAuthMode.EmailLinkSignIn -> AuthRoute.Email.EmailLinkSignIn(email) +} + +/** + * Navigates to [target], replacing any existing entry of the same step *type* rather than stacking + * a duplicate — matching [com.firebase.ui.auth.ui.screens.email.navigateToEmailStep], which the + * library keeps `internal` to its own module. Adds before removing, so no single write leaves the + * stack without the chooser at its base. + */ +private fun MutableList.navigateToStep(target: AuthRoute.Email.Step) { + val existing = indexOfFirst { it is AuthRoute.Email.Step && it::class == target::class } + add(target) + if (existing >= 0) { + while (size > existing + 1) removeAt(existing) + } +} + +/** Matches the 700ms cross-fade [FirebaseAuthScreen][com.firebase.ui.auth.ui.screens.FirebaseAuthScreen] + * itself falls back to, so a step switch here looks the same as one at the top level. */ +private val EmailStepTransform: AnimatedContentTransitionScope>.() -> ContentTransform = { + fadeIn(animationSpec = tween(700)) togetherWith fadeOut(animationSpec = tween(700)) +} + +/** + * [NavDisplay]'s predictive-back default scales the outgoing step down to 70% while the incoming one + * springs in at full size, so a swipe back drew both steps superimposed at different scales. Reuse + * the cross-fade above so a gesture back looks like a tapped one. + */ +private val EmailStepPredictivePopTransform: + AnimatedContentTransitionScope>.(Int) -> ContentTransform = + { EmailStepTransform(this) } + +/** + * Custom UI for `customMethodPickerLayout`'s email path. + * + * Hosts its own [NavDisplay] over [AuthRoute.Email]'s public per-mode destinations, the same + * mechanism [com.firebase.ui.auth.ui.screens.FirebaseAuthScreen] uses for its own hosted + * destinations — so switching between sign-in, sign-up, password reset and email-link sign-in + * animates, gets a real back-stack entry, and never loses the address the user already typed, + * because the address travels as a field on the step's own key rather than in state a switch could + * clear. + */ +@Composable +fun AuthMethodPickerUI( + context: Context, + configuration: AuthUIConfiguration, + authUI: FirebaseAuthUI, + otherProviders: List, + onProviderSelected: (AuthProvider) -> Unit, + onSuccess: (AuthResult) -> Unit, + onError: (AuthException) -> Unit, + onCancel: () -> Unit, +) { + var showOtherMethods by remember { mutableStateOf(false) } + val backStack = rememberNavBackStack(EmailEntryKey) + + Box(modifier = Modifier.fillMaxSize()) { + NavDisplay( + backStack = backStack, + transitionSpec = EmailStepTransform, + popTransitionSpec = EmailStepTransform, + predictivePopTransitionSpec = EmailStepPredictivePopTransform, + entryProvider = entryProvider { + entry { + // No auth operation here, so there is nothing for EmailAuthContentState to own. + var email by rememberSaveable { mutableStateOf("") } + EmailEntryStep( + email = email, + onEmailChange = { email = it }, + isLoading = false, + onSignIn = dropUnlessResumed { + backStack.navigateToStep(AuthRoute.Email.SignIn(email)) + }, + onCreateAccount = dropUnlessResumed { + backStack.navigateToStep(AuthRoute.Email.SignUp(email)) + }, + onShowOtherMethods = { showOtherMethods = true }, + ) + } + + entry { step -> + EmailStep(step, backStack, context, configuration, authUI, onSuccess, onError, onCancel) + } + entry { step -> + EmailStep(step, backStack, context, configuration, authUI, onSuccess, onError, onCancel) + } + entry { step -> + EmailStep(step, backStack, context, configuration, authUI, onSuccess, onError, onCancel) + } + entry { step -> + EmailStep(step, backStack, context, configuration, authUI, onSuccess, onError, onCancel) + } + }, + ) + } + + if (showOtherMethods) { + OtherSignInMethodsSheet( + otherProviders = otherProviders, + onProviderSelected = onProviderSelected, + onDismissRequest = { showOtherMethods = false }, + tosUrl = configuration.tosUrl, + ppUrl = configuration.privacyPolicyUrl, + ) + } +} + +/** + * One [AuthRoute.Email.Step] destination: a single [EmailAuthScreen] instance pinned to [step]'s + * mode, seeded with the address [step] carries. [EmailAuthScreen.onNavigateToMode] is what makes + * a mode switch push (or replace) an entry on [backStack] instead of mutating local state. + */ +@Composable +private fun EmailStep( + step: AuthRoute.Email.Step, + backStack: MutableList, + context: Context, + configuration: AuthUIConfiguration, + authUI: FirebaseAuthUI, + onSuccess: (AuthResult) -> Unit, + onError: (AuthException) -> Unit, + onCancel: () -> Unit, +) { + // Resets rather than pops: from ResetPassword the stack is [chooser, SignIn, ResetPassword]. + val onUseDifferentEmail: () -> Unit = dropUnlessResumed { + backStack.clear() + backStack.add(EmailEntryKey) + } + + EmailAuthScreen( + context = context, + configuration = configuration, + authUI = authUI, + prefillEmail = step.email, + mode = step.toMode(), + onNavigateToMode = { mode, email -> backStack.navigateToStep(stepFor(mode, email)) }, + onSuccess = onSuccess, + onError = onError, + onCancel = onCancel, + ) { state -> + when (state.mode) { + EmailAuthMode.SignUp -> SignUpStep(state, onUseDifferentEmail) + // Reset-password and email-link are inline on the login form, so every mode has a screen. + EmailAuthMode.SignIn, + EmailAuthMode.ResetPassword, + EmailAuthMode.EmailLinkSignIn -> LoginStep(state, onUseDifferentEmail) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthenticatedUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthenticatedUI.kt new file mode 100644 index 0000000000..8fc00b68c3 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthenticatedUI.kt @@ -0,0 +1,287 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens + +import android.util.Log +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.AuthState +import com.firebase.ui.auth.ui.screens.AuthSuccessUiContext +import com.firebase.ui.auth.util.displayIdentifier +import com.firebase.ui.auth.util.getDisplayEmail +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthPage +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +private const val TAG = "FullCustomizationDemo" + +/** + * Custom UI for `FirebaseAuthScreen.authenticatedContent`. + * + * Its main job in this demo is making the other slots reachable: the two-factor button navigates to + * the flow that `mfaEnrollmentContent` renders, and changing the password is a sensitive operation, + * so wrapping it in [com.firebase.ui.auth.FirebaseAuthUI.withReauth] is what provokes + * `reauthContent`. + * + * This slot also receives the email-verification and profile-completion states, which the library + * would otherwise render itself — so they are handled here too rather than falling through to a + * blank screen. + */ +@Composable +fun AuthenticatedUI(state: AuthState, uiContext: AuthSuccessUiContext) { + when (state) { + is AuthState.RequiresEmailVerification -> VerifyEmailPage(uiContext) + is AuthState.RequiresProfileCompletion -> ProfileCompletionPage(state, uiContext) + else -> SignedInPage(uiContext) + } +} + +@Composable +private fun SignedInPage(uiContext: AuthSuccessUiContext) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val authUI = uiContext.authUI + // Not remembered: the identifier changes across sign-out and reauth. + val identifier = authUI.getCurrentUser().displayIdentifier() + + // Refreshes the cached factor list on return from MFA; keyed on Unit so it cannot loop. + LaunchedEffect(Unit) { uiContext.onReloadUser() } + val enrolledFactors = authUI.getCurrentUser()?.multiFactor?.enrolledFactors.orEmpty() + + var newPassword by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } + var isUpdating by remember { mutableStateOf(false) } + var statusMessage by remember { mutableStateOf(null) } + var isError by remember { mutableStateOf(false) } + + AuthPage( + mascot = R.drawable.full_customization_mascot, + mascotDescription = "doggo - cute welcome mascot", + title = "You're in", + cardContentDescription = "authenticated - account card", + card = { + Text( + text = if (identifier.isNotBlank()) "Signed in as $identifier" else "Signed in", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + ) + + Text( + text = "Changing your password needs a recent sign-in, so it triggers the custom " + + "reauth screen.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + FullCustomizationTextField( + value = newPassword, + onValueChange = { + newPassword = it + statusMessage = null + }, + label = "New password", + enabled = !isUpdating, + isError = isError, + supportingText = statusMessage, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) { + Icons.Default.VisibilityOff + } else { + Icons.Default.Visibility + }, + contentDescription = null, + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - new password secure input" }, + ) + }, + actions = { + CtaButton( + text = "Change password", + onClick = { + // lifecycleScope: the reauth overlay replaces this screen; the retry outlives it. + lifecycleOwner.lifecycleScope.launch { + isUpdating = true + statusMessage = null + isError = false + try { + authUI.withReauth( + context, + reason = "Verify your identity to change your password", + ) { + authUI.getCurrentUser()?.updatePassword(newPassword)?.await() + Log.d(TAG, "Password changed successfully") + } + } catch (e: AuthException.AuthCancelledException) { + // Declined, not failed: the password is unchanged, so report neither. + Log.d(TAG, "Reauthentication declined", e) + } catch (e: Exception) { + Log.e(TAG, "Password change failed", e) + isError = true + statusMessage = "Couldn't change the password. Try again." + } finally { + isUpdating = false + } + } + }, + enabled = newPassword.length >= 6 && !isUpdating, + isLoading = isUpdating, + modifier = Modifier.semantics { contentDescription = "button - change password" }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + CtaButton( + // Relabelled, not disabled: SelectFactorStep is the only place a factor can be removed. + text = if (enrolledFactors.isEmpty()) "Set up two-factor" else "Manage two-factor", + onClick = uiContext.onManageMfa, + enabled = !isUpdating, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = Modifier.semantics { contentDescription = "button - manage mfa" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = uiContext.onSignOut, + enabled = !isUpdating, + modifier = Modifier.fillMaxWidth(), + ) { + Text(uiContext.stringProvider.signOutAction) + } + }, + ) +} + +@Composable +private fun VerifyEmailPage(uiContext: AuthSuccessUiContext) { + val stringProvider = uiContext.stringProvider + val user = uiContext.authUI.getCurrentUser() + val emailLabel = user.getDisplayEmail(stringProvider.emailProvider) + + AuthPage( + mascot = R.drawable.full_customization_mascot, + mascotDescription = "doggo - cute welcome mascot", + title = "Check your inbox", + cardContentDescription = "authenticated - verify email card", + card = { + Text( + text = stringProvider.verifyEmailInstruction(emailLabel), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + actions = { + CtaButton( + text = stringProvider.verifiedEmailAction, + onClick = uiContext.onReloadUser, + modifier = Modifier.semantics { + contentDescription = "button - recheck email verification" + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + CtaButton( + text = stringProvider.resendVerificationEmailAction, + onClick = { user?.sendEmailVerification() }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = Modifier.semantics { + contentDescription = "button - resend verification email" + }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = uiContext.onSignOut, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringProvider.signOutAction) + } + }, + ) +} + +@Composable +private fun ProfileCompletionPage( + state: AuthState.RequiresProfileCompletion, + uiContext: AuthSuccessUiContext, +) { + val stringProvider = uiContext.stringProvider + + AuthPage( + mascot = R.drawable.full_customization_mascot, + mascotDescription = "doggo - cute welcome mascot", + title = "Almost there", + cardContentDescription = "authenticated - profile completion card", + card = { + Text( + text = stringProvider.profileCompletionMessage, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + if (state.missingFields.isNotEmpty()) { + Text( + text = stringProvider.profileMissingFieldsMessage( + state.missingFields.joinToString() + ), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + ) + } + }, + actions = { + TextButton( + onClick = uiContext.onSignOut, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringProvider.signOutAction) + } + }, + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/EmailAuthUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/EmailAuthUI.kt new file mode 100644 index 0000000000..06bcecbbe4 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/EmailAuthUI.kt @@ -0,0 +1,83 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.email + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebase.ui.auth.ui.screens.email.EmailAuthMode +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.EmailEntryStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.LoginStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.SignUpStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.reauth.ReauthEmailStep + +/** + * Custom UI for `FirebaseAuthScreen.emailContent`. + * + * The method picker hosts its own email entry, so this slot only renders for email flows the + * *library* navigates to: reauthentication, account linking, and email-already-in-use recovery. + * Without it those flows fall back to the library's stock email screen, which is jarring inside a + * demo whose whole premise is that nothing looks stock. + * + * An address supplied by the library (as reauthentication does) skips the choice entirely — the + * caller already knows who is signing in. + */ +@Composable +fun EmailAuthUI(state: EmailAuthContentState) { + // A fixed address leaves nothing to choose, so render the compact confirm form. + if (state.isEmailLocked) { + ReauthEmailStep(state) + return + } + + var chosen by rememberSaveable { mutableStateOf(state.email.isNotBlank()) } + + val onUseDifferentEmail: () -> Unit = { + state.onPasswordChange("") + state.onConfirmPasswordChange("") + chosen = false + } + + Box(modifier = Modifier.fillMaxSize()) { + // The email pages don't paint their own background, so this slot must. + Image( + painter = painterResource(id = R.drawable.custom_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + + if (!chosen) { + EmailEntryStep( + email = state.email, + onEmailChange = state.onEmailChange, + isLoading = state.isLoading, + onSignIn = { + state.onGoToSignIn() + chosen = true + }, + onCreateAccount = { + state.onGoToSignUp() + chosen = true + }, + // No provider sheet in this slot — the caller already committed to email. + onShowOtherMethods = {}, + ) + } else { + when (state.mode) { + EmailAuthMode.SignUp -> SignUpStep(state, onUseDifferentEmail) + EmailAuthMode.SignIn, + EmailAuthMode.ResetPassword, + EmailAuthMode.EmailLinkSignIn -> LoginStep(state, onUseDifferentEmail) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/EmailEntryStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/EmailEntryStep.kt new file mode 100644 index 0000000000..4b8945ffb8 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/EmailEntryStep.kt @@ -0,0 +1,197 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Login +import androidx.compose.material3.Icon +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.EmailFieldIcon +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow +import com.firebaseui.android.demo.auth.fullcustomization.theme.IntroShape + +@Composable +fun EmailEntryStep( + email: String, + onEmailChange: (String) -> Unit, + isLoading: Boolean, + onSignIn: () -> Unit, + onCreateAccount: () -> Unit, + onShowOtherMethods: () -> Unit, +) { + val isEmailValid = remember(email) { + android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() + } + val showEmailError = email.isNotBlank() && !isEmailValid + + // heightIn(min = maxHeight) centres content when it fits and plain-scrolls when it doesn't. + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 48.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.full_customization_mascot), + contentDescription = "doggo - cute welcome mascot", + modifier = Modifier + .size(96.dp) + .offset(y = 12.dp) + .zIndex(1f), + ) + + Surface( + color = MaterialTheme.colorScheme.secondary, + shape = IntroShape, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "intro - welcome headline bubble" }, + ) { + Text( + text = "Hey there,\nWelcome", + style = MaterialTheme.typography.headlineMedium.copy( + textAlign = TextAlign.Center, + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFFF8F8), + Color(0xFFFFDDB4), + Color(0xFFFFD8EB), + ), + ), + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 16.dp), + ) + } + + HardOffsetShadow( + shape = AuthFieldShape, + modifier = Modifier.fillMaxWidth(), + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "email - sign in card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "Enter your email address to continue.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + + FullCustomizationTextField( + value = email, + onValueChange = onEmailChange, + label = "Email address", + leadingIcon = { EmailFieldIcon() }, + enabled = !isLoading, + isError = showEmailError, + supportingText = if (showEmailError) "Enter a valid email address" else null, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - email address input" }, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Two explicit choices: enumeration protection withholds whether an address is registered. + CtaButton( + text = "Sign in", + onClick = onSignIn, + enabled = isEmailValid && !isLoading, + isLoading = isLoading, + modifier = Modifier.semantics { contentDescription = "button - sign in" }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + CtaButton( + text = "Create account", + onClick = onCreateAccount, + // Not gated on the address: the sign-up form collects and confirms it. + enabled = !isLoading, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = Modifier.semantics { contentDescription = "button - create account" }, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + TextButton( + onClick = onShowOtherMethods, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .semantics { contentDescription = "Other sign-in methods button" }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Login, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Use other sign-in methods") + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/LoginStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/LoginStep.kt new file mode 100644 index 0000000000..769e3063af --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/LoginStep.kt @@ -0,0 +1,189 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.EmailFieldIcon +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +@Composable +fun LoginStep( + state: EmailAuthContentState, + onUseDifferentEmail: () -> Unit, +) { + var passwordVisible by remember { mutableStateOf(false) } + + // heightIn(min = maxHeight) centres content when it fits and plain-scrolls when it doesn't. + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 40.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(id = R.drawable.full_customization_mascot), + contentDescription = "doggo - cute welcome mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Login", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "email - login card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + FullCustomizationTextField( + value = state.email, + onValueChange = {}, + enabled = false, + leadingIcon = { EmailFieldIcon() }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - email address display" }, + ) + + FullCustomizationTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = "Password", + enabled = !state.isLoading, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) { + Icons.Default.VisibilityOff + } else { + Icons.Default.Visibility + }, + contentDescription = null, + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - password secure input" }, + ) + + Text( + text = if (state.resetLinkSent) "Reset link sent!" else "Forgot password?", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = !state.resetLinkSent) { + state.onSendResetLinkClick() + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + CtaButton( + text = "Login", + onClick = state.onSignInClick, + enabled = state.password.isNotBlank() && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - login" }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + CtaButton( + text = if (state.emailSignInLinkSent) "Login link sent!" else "Send login link", + onClick = state.onSignInEmailLinkClick, + enabled = !state.isLoading, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = Modifier.semantics { contentDescription = "button - send login link" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onUseDifferentEmail, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Use a different email") + } + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/SignUpStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/SignUpStep.kt new file mode 100644 index 0000000000..7f7dd2dff5 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/SignUpStep.kt @@ -0,0 +1,237 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.EmailFieldIcon +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +private val NameFieldStartShape = RoundedCornerShape( + topStart = 16.dp, + bottomStart = 16.dp, + topEnd = 0.dp, + bottomEnd = 0.dp, +) +private val NameFieldEndShape = RoundedCornerShape( + topStart = 0.dp, + bottomStart = 0.dp, + topEnd = 16.dp, + bottomEnd = 16.dp, +) + +@Composable +fun SignUpStep( + state: EmailAuthContentState, + onUseDifferentEmail: () -> Unit, +) { + var firstName by remember { mutableStateOf("") } + var lastName by remember { mutableStateOf("") } + var confirmEmail by remember { mutableStateOf("") } + + // Trimmed and case-insensitive: the default keyboard auto-capitalises on many IMEs. + val emailsMatch = confirmEmail.isNotBlank() && + confirmEmail.trim().equals(state.email.trim(), ignoreCase = true) + val passwordsMatch = state.confirmPassword.isNotBlank() && state.confirmPassword == state.password + val canSignUp = firstName.isNotBlank() && + lastName.isNotBlank() && + emailsMatch && + state.password.isNotBlank() && + passwordsMatch && + !state.isLoading + + // heightIn(min = maxHeight) centres content when it fits and plain-scrolls when it doesn't. + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 40.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(id = R.drawable.full_customization_mascot), + contentDescription = "doggo - cute welcome mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Sign up", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "sign up card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Row(modifier = Modifier.fillMaxWidth()) { + FullCustomizationTextField( + value = firstName, + onValueChange = { firstName = it }, + label = "First name", + enabled = !state.isLoading, + shape = NameFieldStartShape, + modifier = Modifier + .weight(1f) + .semantics { contentDescription = "text-field - first name" }, + ) + FullCustomizationTextField( + value = lastName, + onValueChange = { lastName = it }, + label = "Last name", + enabled = !state.isLoading, + shape = NameFieldEndShape, + modifier = Modifier + .weight(1f) + .semantics { contentDescription = "text-field - last name" }, + ) + } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + FullCustomizationTextField( + value = state.email, + // Editable unlike the login form: the account does not exist yet. + onValueChange = state.onEmailChange, + label = "Email", + enabled = !state.isLoading, + leadingIcon = { EmailFieldIcon() }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + ), + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - email address display" }, + ) + FullCustomizationTextField( + value = confirmEmail, + onValueChange = { confirmEmail = it }, + label = "Confirm Email", + enabled = !state.isLoading, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + ), + isError = confirmEmail.isNotBlank() && !emailsMatch, + supportingText = if (confirmEmail.isNotBlank() && !emailsMatch) { + "Emails don't match" + } else { + null + }, + leadingIcon = { EmailFieldIcon() }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - confirm email" }, + ) + } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + FullCustomizationTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = "Password", + enabled = !state.isLoading, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - password" }, + ) + FullCustomizationTextField( + value = state.confirmPassword, + onValueChange = state.onConfirmPasswordChange, + label = "Confirm Password", + enabled = !state.isLoading, + visualTransformation = PasswordVisualTransformation(), + isError = state.confirmPassword.isNotBlank() && !passwordsMatch, + supportingText = if (state.confirmPassword.isNotBlank() && !passwordsMatch) { + "Passwords don't match" + } else { + null + }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - confirm password" }, + ) + } + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + CtaButton( + text = "Sign up", + onClick = { + state.onDisplayNameChange("$firstName $lastName".trim()) + state.onSignUpClick() + }, + enabled = canSignUp, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - sign up" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onUseDifferentEmail, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Use a different email") + } + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/MfaChallengeUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/MfaChallengeUI.kt new file mode 100644 index 0000000000..741f41882d --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/MfaChallengeUI.kt @@ -0,0 +1,101 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.mfa + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.MfaFactor +import com.firebase.ui.auth.mfa.MfaChallengeContentState +import com.firebase.ui.auth.ui.components.VerificationCodeInputField +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthPage +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton + +/** + * Custom UI for `FirebaseAuthScreen.mfaChallengeContent` — the second-factor prompt shown during + * sign-in when the account has MFA enrolled. + */ +@Composable +fun MfaChallengeUI(state: MfaChallengeContentState) { + val isSms = state.factorType == MfaFactor.Sms + + AuthPage( + mascot = if (isSms) { + R.drawable.full_customization_phone_mascot + } else { + R.drawable.full_customization_mascot + }, + mascotDescription = "doggo - cute two-factor mascot", + title = "One more step", + cardContentDescription = "mfa - challenge card", + card = { + Text( + text = if (isSms) { + "We sent a code to ${state.maskedPhoneNumber ?: "your phone"}." + } else { + "Open your authenticator app and enter the 6-digit code for this account." + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + VerificationCodeInputField( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - mfa challenge code input" }, + isError = state.hasError, + errorMessage = state.error, + onCodeChange = state.onVerificationCodeChange, + ) + + // canResend already covers "SMS factor and a resend callback exists". + if (state.canResend) { + Text( + text = if (state.resendTimer > 0) { + "Resend code in ${state.resendTimer}s" + } else { + "Resend code" + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = state.resendTimer == 0 && !state.isLoading) { + state.onResendCodeClick?.invoke() + }, + ) + } + }, + actions = { + CtaButton( + text = "Verify", + onClick = state.onVerifyClick, + enabled = state.isValid && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { + contentDescription = "button - verify mfa challenge" + }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = state.onCancelClick, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Cancel") + } + }, + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/MfaEnrollmentUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/MfaEnrollmentUI.kt new file mode 100644 index 0000000000..f104c1bb00 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/MfaEnrollmentUI.kt @@ -0,0 +1,25 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.mfa + +import androidx.compose.runtime.Composable +import com.firebase.ui.auth.mfa.MfaEnrollmentContentState +import com.firebase.ui.auth.mfa.MfaEnrollmentStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages.ConfigureSmsStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages.ConfigureTotpStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages.SelectFactorStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages.VerifyFactorStep + +/** + * Custom UI for `FirebaseAuthScreen.mfaEnrollmentContent`. + * + * A single state object drives every enrollment step, so this only dispatches on + * [MfaEnrollmentContentState.step] — the library owns the step transitions. + */ +@Composable +fun MfaEnrollmentUI(state: MfaEnrollmentContentState) { + when (state.step) { + MfaEnrollmentStep.SelectFactor -> SelectFactorStep(state) + MfaEnrollmentStep.ConfigureSms -> ConfigureSmsStep(state) + MfaEnrollmentStep.ConfigureTotp -> ConfigureTotpStep(state) + MfaEnrollmentStep.VerifyFactor -> VerifyFactorStep(state) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/ConfigureSmsStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/ConfigureSmsStep.kt new file mode 100644 index 0000000000..d73284fb9c --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/ConfigureSmsStep.kt @@ -0,0 +1,123 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.mfa.MfaEnrollmentContentState +import com.firebase.ui.auth.ui.components.CountrySelector +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthPage +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField + +@Composable +fun ConfigureSmsStep(state: MfaEnrollmentContentState) { + AuthPage( + mascot = R.drawable.full_customization_phone_mascot, + mascotDescription = "doggo - cute phone sign-in mascot", + title = "Add your number", + cardContentDescription = "mfa - sms setup card", + card = { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + // CountrySelector needs a non-null country, so skip the step while it resolves. + state.selectedCountry?.let { country -> + Surface( + color = Color.White, + shape = AuthFieldShape, + modifier = Modifier + .requiredHeight(56.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = AuthFieldShape, + ) + .semantics { contentDescription = "country code selector" }, + ) { + CountrySelector( + selectedCountry = country, + onCountrySelected = state.onCountrySelected, + enabled = !state.isLoading, + ) + } + } + + FullCustomizationTextField( + value = state.phoneNumber, + onValueChange = state.onPhoneNumberChange, + placeholder = "Phone number", + leadingIcon = { + Icon( + imageVector = Icons.Default.Phone, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + enabled = !state.isLoading, + isError = state.hasError, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + modifier = Modifier + .weight(1f) + .semantics { contentDescription = "text-field - mfa phone number input" }, + ) + } + + Text( + text = state.error + ?: "We'll text a code to this number whenever you sign in. " + + "Message & data rates may apply.", + style = MaterialTheme.typography.bodyLarge, + color = if (state.hasError) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + }, + actions = { + CtaButton( + text = "Send code", + onClick = state.onSendSmsCodeClick, + enabled = state.isValid && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { + contentDescription = "button - send mfa sms code" + }, + ) + + if (state.canGoBack) { + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = state.onBackClick, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Pick a different method") + } + } + }, + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/ConfigureTotpStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/ConfigureTotpStep.kt new file mode 100644 index 0000000000..562fe7919a --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/ConfigureTotpStep.kt @@ -0,0 +1,102 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.mfa.MfaEnrollmentContentState +import com.firebase.ui.auth.ui.components.QrCodeImage +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthPage +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton + +@Composable +fun ConfigureTotpStep(state: MfaEnrollmentContentState) { + AuthPage( + mascot = R.drawable.full_customization_mascot, + mascotDescription = "doggo - cute security mascot", + title = "Scan to set up", + cardContentDescription = "mfa - totp setup card", + card = { + Text( + text = "Scan this with your authenticator app, or type the key in by hand.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + state.totpQrCodeUrl?.let { url -> + QrCodeImage( + content = url, + size = 200.dp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = AuthFieldShape, + ) + .padding(12.dp) + .semantics { contentDescription = "mfa - totp qr code" }, + ) + } + + state.totpSecret?.sharedSecretKey?.let { key -> + SelectionContainer { + Text( + text = key, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "mfa - totp shared secret key" }, + ) + } + } + + state.error?.let { error -> + Text( + text = error, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + ) + } + }, + actions = { + CtaButton( + text = "I've added it", + onClick = state.onContinueToVerifyClick, + enabled = state.isValid && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { + contentDescription = "button - continue to mfa verification" + }, + ) + + if (state.canGoBack) { + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = state.onBackClick, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Pick a different method") + } + } + }, + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/SelectFactorStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/SelectFactorStep.kt new file mode 100644 index 0000000000..70995a799e --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/SelectFactorStep.kt @@ -0,0 +1,141 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.MfaFactor +import com.firebase.ui.auth.mfa.MfaEnrollmentContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthPage +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.google.firebase.auth.MultiFactorInfo +import com.google.firebase.auth.PhoneMultiFactorGenerator +import com.google.firebase.auth.TotpMultiFactorGenerator + +@Composable +fun SelectFactorStep(state: MfaEnrollmentContentState) { + AuthPage( + mascot = R.drawable.full_customization_mascot, + mascotDescription = "doggo - cute security mascot", + title = "Secure your account", + cardContentDescription = "mfa - factor selection card", + card = { + Text( + text = "Add a second step to sign-in, so a password on its own isn't enough.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + state.error?.let { error -> + Text( + text = error, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + ) + } + + if (state.enrolledFactors.isNotEmpty()) { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Already on this account", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + ) + + state.enrolledFactors.forEach { info -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = enrolledFactorLabel(info), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), + ) + TextButton( + onClick = { state.onUnenrollFactor(info) }, + enabled = !state.isLoading, + modifier = Modifier.semantics { + contentDescription = + "button - remove factor ${enrolledFactorLabel(info)}" + }, + ) { + Text("Remove") + } + } + } + } + } + }, + actions = { + state.availableFactors.forEachIndexed { index, factor -> + if (index > 0) Spacer(modifier = Modifier.height(16.dp)) + + CtaButton( + text = factorCtaLabel(factor), + onClick = { state.onFactorSelected(factor) }, + enabled = !state.isLoading, + // First factor takes the primary CTA colour; the rest read as alternatives. + colors = if (index == 0) { + ButtonDefaults.buttonColors() + } else { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + }, + modifier = Modifier.semantics { + contentDescription = "button - enroll ${factorCtaLabel(factor)}" + }, + ) + } + + state.onSkipClick?.let { onSkip -> + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onSkip, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Not now") + } + } + }, + ) +} + +private fun factorCtaLabel(factor: MfaFactor): String = when (factor) { + MfaFactor.Sms -> "Use text message" + MfaFactor.Totp -> "Use an authenticator app" +} + +/** + * SMS factors carry the phone number as their display name; TOTP factors are often unnamed, so + * fall back to the factor id. + */ +private fun enrolledFactorLabel(info: MultiFactorInfo): String { + val fallback = when (info.factorId) { + PhoneMultiFactorGenerator.FACTOR_ID -> "Text message" + TotpMultiFactorGenerator.FACTOR_ID -> "Authenticator app" + else -> info.factorId + } + return info.displayName?.takeIf { it.isNotBlank() } ?: fallback +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/VerifyFactorStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/VerifyFactorStep.kt new file mode 100644 index 0000000000..351c73cfc5 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/mfa/pages/VerifyFactorStep.kt @@ -0,0 +1,100 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.pages + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.MfaFactor +import com.firebase.ui.auth.mfa.MfaEnrollmentContentState +import com.firebase.ui.auth.ui.components.VerificationCodeInputField +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthPage +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton + +@Composable +fun VerifyFactorStep(state: MfaEnrollmentContentState) { + val isSms = state.selectedFactor == MfaFactor.Sms + val fullPhoneNumber = "${state.selectedCountry?.dialCode ?: ""}${state.phoneNumber}" + + AuthPage( + mascot = if (isSms) { + R.drawable.full_customization_phone_mascot + } else { + R.drawable.full_customization_mascot + }, + mascotDescription = "doggo - cute two-factor mascot", + title = "Confirm the code", + cardContentDescription = "mfa - enrollment verification card", + card = { + Text( + text = if (isSms) { + "We sent a code to $fullPhoneNumber." + } else { + "Enter the 6-digit code your authenticator app is showing right now." + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + VerificationCodeInputField( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - mfa enrollment code input" }, + isError = state.hasError, + errorMessage = state.error, + onCodeChange = state.onVerificationCodeChange, + ) + + // onResendCodeClick is null for TOTP, where there is nothing to resend. + state.onResendCodeClick?.let { onResend -> + Text( + text = if (state.resendTimer > 0) { + "Resend code in ${state.resendTimer}s" + } else { + "Resend code" + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = state.resendTimer == 0 && !state.isLoading) { + onResend() + }, + ) + } + }, + actions = { + CtaButton( + text = "Verify", + onClick = state.onVerifyClick, + enabled = state.isValid && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { + contentDescription = "button - verify mfa enrollment" + }, + ) + + if (state.canGoBack) { + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = state.onBackClick, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Back") + } + } + }, + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/PhoneSignInUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/PhoneSignInUI.kt new file mode 100644 index 0000000000..ecf4bc41a1 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/PhoneSignInUI.kt @@ -0,0 +1,30 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.phone + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthStep +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages.PhoneEntryStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages.PhoneVerificationStep + +@Composable +fun PhoneSignInUI(state: PhoneAuthContentState) { + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(id = R.drawable.custom_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + when (state.step) { + PhoneAuthStep.EnterPhoneNumber -> PhoneEntryStep(state) + PhoneAuthStep.EnterVerificationCode -> PhoneVerificationStep(state) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneEntryStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneEntryStep.kt new file mode 100644 index 0000000000..87e7d8ed45 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneEntryStep.kt @@ -0,0 +1,162 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.components.CountrySelector +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +@Composable +fun PhoneEntryStep(state: PhoneAuthContentState) { + val isPhoneValid = remember(state.phoneNumber) { + android.util.Patterns.PHONE.matcher(state.phoneNumber).matches() + } + + // heightIn(min = maxHeight) centres content when it fits and plain-scrolls when it doesn't. + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 40.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(id = R.drawable.full_customization_phone_mascot), + contentDescription = "doggo - cute phone sign-in mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Login by phone number", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "phone - sign in card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Surface( + color = Color.White, + shape = AuthFieldShape, + modifier = Modifier + .requiredHeight(56.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = AuthFieldShape, + ) + .semantics { contentDescription = "country code selector" }, + ) { + CountrySelector( + selectedCountry = state.selectedCountry, + onCountrySelected = state.onCountrySelected, + enabled = !state.isLoading, + ) + } + + FullCustomizationTextField( + value = state.phoneNumber, + onValueChange = state.onPhoneNumberChange, + placeholder = "Phone number", + leadingIcon = { + Icon( + imageVector = Icons.Default.Phone, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + enabled = !state.isLoading, + isError = state.error != null, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + modifier = Modifier + .weight(1f) + .semantics { contentDescription = "text-field - phone number input" }, + ) + } + + Text( + text = state.error + ?: "By signing in with phone number, an SMS may be sent. " + + "Message & data rates may apply.", + style = MaterialTheme.typography.bodyLarge, + color = if (state.error != null) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + CtaButton( + text = "Send code", + onClick = state.onSendCodeClick, + enabled = isPhoneValid && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - send verification code" }, + ) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneVerificationStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneVerificationStep.kt new file mode 100644 index 0000000000..8e9a153182 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneVerificationStep.kt @@ -0,0 +1,137 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.components.VerificationCodeInputField +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +@Composable +fun PhoneVerificationStep(state: PhoneAuthContentState) { + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 40.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(id = R.drawable.full_customization_phone_mascot), + contentDescription = "doggo - cute phone sign-in mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Enter your code", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "phone - verification card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "We sent a code to ${state.fullPhoneNumber}.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + VerificationCodeInputField( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - verification code input" }, + isError = state.error != null, + errorMessage = state.error, + onCodeChange = state.onVerificationCodeChange, + ) + + Text( + text = if (state.resendTimer > 0) { + "Resend code in ${state.resendTimer}s" + } else { + "Resend code" + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = state.resendTimer == 0) { + state.onResendCodeClick() + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + CtaButton( + text = "Verify", + onClick = state.onVerifyCodeClick, + enabled = state.verificationCode.isNotBlank() && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - verify code" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = state.onChangeNumberClick, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Use a different number") + } + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/reauth/ReauthEmailStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/reauth/ReauthEmailStep.kt new file mode 100644 index 0000000000..d22ee432be --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/reauth/ReauthEmailStep.kt @@ -0,0 +1,143 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.reauth + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField + +/** + * The email step of reauthentication, for `FirebaseAuthScreen.emailContent` when the library has + * locked the address. + * + * Sign-in's own page is the wrong screen here twice over. The library composes the reauth email + * step inside a modal bottom sheet, so a full-bleed background and a viewport-height layout fight + * the sheet rather than sit in it; and reauthentication turns off sign-up and email-link sign-in + * (`isEmailSignUpOffered`/`isEmailLinkSignInOffered` both return false in that mode), so the + * affordances that page offers alongside the password are dead. This is the one thing the user can + * actually do: confirm the password for an address they cannot change. + * + * Password reset stays, because it still works — the link is sent while the sheet is up. + */ +@Composable +fun ReauthEmailStep(state: EmailAuthContentState) { + var passwordVisible by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .imePadding() + .padding(horizontal = 24.dp) + .padding(bottom = 24.dp) + .semantics { contentDescription = "reauth - email password card" }, + ) { + Image( + painter = painterResource(id = R.drawable.full_customization_mascot), + contentDescription = "doggo - cute welcome mascot", + modifier = Modifier.size(56.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Confirm it's you", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Enter the password for ${state.email}.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + FullCustomizationTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = "Password", + enabled = !state.isLoading, + isError = state.error != null, + supportingText = state.error, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) { + Icons.Default.VisibilityOff + } else { + Icons.Default.Visibility + }, + contentDescription = null, + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - reauth password secure input" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = if (state.resetLinkSent) "Reset link sent!" else "Forgot password?", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = !state.resetLinkSent && !state.isLoading) { + state.onSendResetLinkClick() + }, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + CtaButton( + text = "Confirm", + onClick = state.onSignInClick, + enabled = state.password.isNotBlank() && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - confirm reauth" }, + ) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/reauth/ReauthUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/reauth/ReauthUI.kt new file mode 100644 index 0000000000..557cc79c53 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/reauth/ReauthUI.kt @@ -0,0 +1,92 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.reauth + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.screens.reauth.ReauthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthPage +import com.firebaseui.android.demo.auth.fullcustomization.common.SheetProviderButton + +/** + * Custom UI for `FirebaseAuthScreen.reauthContent`. + * + * [ReauthContentState.providers] arrives already filtered to the providers linked to this user, and + * [ReauthContentState.onProviderSelected] performs the credential exchange, so this is purely a + * chooser: the library owns the reauthentication itself and the dismiss/retry sequencing that + * follows it. Picking email or phone hands off to the library's own sub-flow. + */ +@Composable +fun ReauthUI(state: ReauthContentState) { + // Overlay outside the NavHost: without this, back would finish the Activity mid-reauth. + BackHandler(enabled = !state.isLoading) { state.onDismiss() } + + AuthPage( + mascot = R.drawable.full_customization_mascot, + mascotDescription = "doggo - cute welcome mascot", + title = "Is that you?", + cardContentDescription = "reauth - provider chooser card", + card = { + Text( + text = state.reason + ?: "Confirm it's you to continue with ${state.user.email ?: "this account"}.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + state.error?.let { error -> + Text( + text = error, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + ) + } + + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .semantics { contentDescription = "reauth - in progress" }, + ) + } + }, + actions = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + state.providers.forEach { provider -> + SheetProviderButton( + provider = provider, + onClick = { state.onProviderSelected(provider) }, + modifier = Modifier + .fillMaxWidth() + .semantics { + contentDescription = "button - reauth with ${provider.providerName}" + }, + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = state.onDismiss, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Cancel") + } + }, + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationShapes.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationShapes.kt new file mode 100644 index 0000000000..483b0b741d --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationShapes.kt @@ -0,0 +1,8 @@ +package com.firebaseui.android.demo.auth.fullcustomization.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.unit.dp + +val IntroShape = RoundedCornerShape(80.dp) +val ButtonShape = RoundedCornerShape(36.dp) +val ProviderButtonShape = RoundedCornerShape(percent = 50) diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTheme.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTheme.kt new file mode 100644 index 0000000000..53b9161191 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTheme.kt @@ -0,0 +1,135 @@ +package com.firebaseui.android.demo.auth.fullcustomization.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import kotlin.math.max +import kotlin.math.min + +private val LightPrimary = Color(0xFF864B6F) +private val LightOnPrimary = Color(0xFFFFFFFF) +private val LightPrimaryContainer = Color(0xFFFFD8EB) +private val LightOnPrimaryContainer = Color(0xFF7B3B73) +private val LightInversePrimary = Color(0xFFFAB1DA) +private val LightSecondary = Color(0xFF4C8BFF) +private val LightOnSecondary = Color(0xFFFFFFFF) +private val LightSecondaryContainer = Color(0xFFCCE5FF) +private val LightTertiaryContainer = Color(0xFFFFDDB4) +private val LightSurface = Color(0xFFFFF8F8) +private val LightSurfaceBright = Color(0xFFFFF8F8) +private val LightOnSurface = Color(0xFF211A1D) +private val LightOnSurfaceVariant = Color(0xFFA08B95) +private val LightSurfaceContainer = Color(0xFFF9EAEF) +private val LightSurfaceContainerLow = Color(0xFFFDF0F6) +private val LightOutline = Color(0xFF81737A) +private val LightOutlineVariant = Color(0xFFD3C2C9) +private val LightInverseSurface = Color(0xFF322F35) +private val LightInverseOnSurface = Color(0xFFF5EFF7) + +val FullCustomizationLightColorScheme = lightColorScheme( + primary = LightPrimary, + onPrimary = LightOnPrimary, + primaryContainer = LightPrimaryContainer, + onPrimaryContainer = LightOnPrimaryContainer, + inversePrimary = LightInversePrimary, + secondary = LightSecondary, + onSecondary = LightOnSecondary, + secondaryContainer = LightSecondaryContainer, + tertiaryContainer = LightTertiaryContainer, + surface = LightSurface, + surfaceBright = LightSurfaceBright, + onSurface = LightOnSurface, + onSurfaceVariant = LightOnSurfaceVariant, + surfaceContainer = LightSurfaceContainer, + surfaceContainerLow = LightSurfaceContainerLow, + outline = LightOutline, + outlineVariant = LightOutlineVariant, + inverseSurface = LightInverseSurface, + inverseOnSurface = LightInverseOnSurface, +) + +val FullCustomizationDarkColorScheme = darkColorScheme( + primary = LightPrimary.withLightness(0.78f), + onPrimary = LightOnPrimary.withLightness(0.18f), + primaryContainer = LightPrimaryContainer.withLightness(0.28f), + onPrimaryContainer = LightOnPrimaryContainer.withLightness(0.88f), + inversePrimary = LightPrimary, + secondary = LightSecondary.withLightness(0.78f), + onSecondary = LightOnSecondary.withLightness(0.18f), + secondaryContainer = LightSecondaryContainer.withLightness(0.28f), + tertiaryContainer = LightTertiaryContainer.withLightness(0.28f), + surface = LightSurface.withLightness(0.10f), + surfaceBright = LightSurfaceBright.withLightness(0.20f), + onSurface = LightOnSurface.withLightness(0.88f), + onSurfaceVariant = LightOnSurfaceVariant.withLightness(0.75f), + surfaceContainer = LightSurfaceContainer.withLightness(0.13f), + surfaceContainerLow = LightSurfaceContainerLow.withLightness(0.11f), + outline = LightOutline.withLightness(0.55f), + outlineVariant = LightOutlineVariant.withLightness(0.30f), + inverseSurface = LightSurface.withLightness(0.90f), + inverseOnSurface = LightOnSurface.withLightness(0.15f), +) + +@Composable +fun FullCustomizationTheme(content: @Composable () -> Unit) { + val colorScheme = if (isSystemInDarkTheme()) { + FullCustomizationDarkColorScheme + } else { + FullCustomizationLightColorScheme + } + AuthUITheme( + theme = AuthUITheme.Default.copy( + colorScheme = colorScheme, + typography = FullCustomizationTypography, + providerButtonShape = ProviderButtonShape, + ), + content = content, + ) +} + +private fun Color.withLightness(newLightness: Float): Color { + val (h, s, _) = toHsl() + return hslToColor(h, s, newLightness.coerceIn(0f, 1f), alpha) +} + +private fun Color.toHsl(): Triple { + val r = red + val g = green + val b = blue + val maxC = max(r, max(g, b)) + val minC = min(r, min(g, b)) + val l = (maxC + minC) / 2f + if (maxC == minC) return Triple(0f, 0f, l) + val d = maxC - minC + val s = if (l > 0.5f) d / (2f - maxC - minC) else d / (maxC + minC) + val h = when (maxC) { + r -> ((g - b) / d + (if (g < b) 6f else 0f)) + g -> ((b - r) / d + 2f) + else -> ((r - g) / d + 4f) + } / 6f + return Triple(h, s, l) +} + +private fun hslToColor(h: Float, s: Float, l: Float, alpha: Float): Color { + if (s == 0f) return Color(l, l, l, alpha) + fun hueToRgb(p: Float, q: Float, tIn: Float): Float { + var t = tIn + if (t < 0f) t += 1f + if (t > 1f) t -= 1f + return when { + t < 1f / 6f -> p + (q - p) * 6f * t + t < 1f / 2f -> q + t < 2f / 3f -> p + (q - p) * (2f / 3f - t) * 6f + else -> p + } + } + val q = if (l < 0.5f) l * (1f + s) else l + s - l * s + val p = 2f * l - q + val r = hueToRgb(p, q, h + 1f / 3f) + val g = hueToRgb(p, q, h) + val b = hueToRgb(p, q, h - 1f / 3f) + return Color(r, g, b, alpha) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTypography.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTypography.kt new file mode 100644 index 0000000000..da31b8bc23 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTypography.kt @@ -0,0 +1,59 @@ +package com.firebaseui.android.demo.auth.fullcustomization.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.firebaseui.android.demo.R + +val BagelFatOne = FontFamily(Font(R.font.bagel_fat_one_regular, FontWeight.Normal)) + +val Onest = FontFamily( + Font(R.font.onest_regular, FontWeight.Normal), + Font(R.font.onest_medium, FontWeight.Medium), + Font(R.font.onest_semibold, FontWeight.SemiBold), + Font(R.font.onest_bold, FontWeight.Bold), +) + +val Roboto = FontFamily( + Font(R.font.roboto_regular, FontWeight.Normal), + Font(R.font.roboto_medium, FontWeight.Medium), + Font(R.font.roboto_semibold, FontWeight.SemiBold), + Font(R.font.roboto_bold, FontWeight.Bold), +) + +val FullCustomizationTypography = Typography( + headlineSmall = TextStyle( + fontFamily = BagelFatOne, + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + ), + headlineMedium = TextStyle( + fontFamily = BagelFatOne, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + ), + bodyLarge = TextStyle( + fontFamily = Onest, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + ), + labelLarge = TextStyle( + fontFamily = Roboto, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + titleMedium = TextStyle( + fontFamily = Onest, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + lineHeight = 20.sp, + ), +) diff --git a/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt index 4e99cbfd33..b756c2276f 100644 --- a/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.firebase.ui.database.paging.DatabasePagingOptions import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter +import com.firebaseui.android.demo.R import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.DatabaseReference import com.google.firebase.database.FirebaseDatabase @@ -122,7 +123,8 @@ class ScoreAdapter(options: DatabasePagingOptions) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ScoreViewHolder(parent) override fun onBindViewHolder(holder: ScoreViewHolder, position: Int, model: ScoreItem) { - (holder.itemView as TextView).text = "${model.name} — score: ${model.score}" + val row = holder.itemView as TextView + row.text = row.context.getString(R.string.demo_score_row, model.name, model.score) } } diff --git a/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt index 91d07abb91..3fb4e44d5b 100644 --- a/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.firebase.ui.firestore.paging.FirestorePagingAdapter import com.firebase.ui.firestore.paging.FirestorePagingOptions +import com.firebaseui.android.demo.R import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.FirebaseFirestore @@ -129,7 +130,8 @@ class ScoreAdapter(options: FirestorePagingOptions) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ScoreViewHolder(parent) override fun onBindViewHolder(holder: ScoreViewHolder, position: Int, model: ScoreItem) { - (holder.itemView as TextView).text = "${model.name} — score: ${model.score}" + val row = holder.itemView as TextView + row.text = row.context.getString(R.string.demo_score_row, model.name, model.score) } } diff --git a/app/src/main/java/com/firebaseui/android/demo/utils/EmailLinkRouting.kt b/app/src/main/java/com/firebaseui/android/demo/utils/EmailLinkRouting.kt new file mode 100644 index 0000000000..d881baad43 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/utils/EmailLinkRouting.kt @@ -0,0 +1,33 @@ +package com.firebaseui.android.demo.utils + +import android.net.Uri +import androidx.core.net.toUri + +/** + * Works out which auth demo a returning email link came from. + * + * Demo plumbing, not an example of library usage: this app has several auth demos behind one deep + * link, so it has to tag its own links and route the return trip. An app with a single sign-in + * screen needs none of this — it passes the link straight to `FirebaseAuthScreen(emailLink = …)`. + */ + +/** The query parameter each demo tags its own continue URL with. */ +const val EMAIL_LINK_ORIGIN_PARAM = "demo" + +/** The continue URL, which sits either directly on [uri] or nested inside its `link`. */ +private fun continueUrlOf(uri: Uri): Uri? { + uri.getQueryParameter("continueUrl")?.let { return it.toUri() } + uri.getQueryParameter("link")?.let { return continueUrlOf(it.toUri()) } + return null +} + +/** + * Which demo sent [link], or null when it says nothing about where it came from. + * + * Read off the continue URL rather than the outer link, so the library's own `ui_` parameters and + * any the action handler adds cannot be mistaken for the tag. + */ +internal fun emailLinkOrigin(link: String?): String? = + link?.takeIf { it.isNotEmpty() } + ?.let { runCatching { continueUrlOf(it.toUri()) }.getOrNull() } + ?.getQueryParameter(EMAIL_LINK_ORIGIN_PARAM) diff --git a/app/src/main/res/drawable-hdpi/firebase_auth.png b/app/src/main/res/drawable-hdpi/firebase_auth.png deleted file mode 100644 index fecbcb6dd4..0000000000 Binary files a/app/src/main/res/drawable-hdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/firebase_auth.webp b/app/src/main/res/drawable-hdpi/firebase_auth.webp new file mode 100644 index 0000000000..ecdf81adb5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-mdpi/firebase_auth.png b/app/src/main/res/drawable-mdpi/firebase_auth.png deleted file mode 100644 index bc9af3cc0c..0000000000 Binary files a/app/src/main/res/drawable-mdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/firebase_auth.webp b/app/src/main/res/drawable-mdpi/firebase_auth.webp new file mode 100644 index 0000000000..cb5675df38 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-nodpi/custom_background.webp b/app/src/main/res/drawable-nodpi/custom_background.webp new file mode 100644 index 0000000000..c92b00b9ce Binary files /dev/null and b/app/src/main/res/drawable-nodpi/custom_background.webp differ diff --git a/app/src/main/res/drawable-nodpi/email_at_sign.webp b/app/src/main/res/drawable-nodpi/email_at_sign.webp new file mode 100644 index 0000000000..dd61129ec7 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/email_at_sign.webp differ diff --git a/app/src/main/res/drawable-nodpi/full_customization_mascot.webp b/app/src/main/res/drawable-nodpi/full_customization_mascot.webp new file mode 100644 index 0000000000..4655b76464 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/full_customization_mascot.webp differ diff --git a/app/src/main/res/drawable-nodpi/full_customization_phone_mascot.webp b/app/src/main/res/drawable-nodpi/full_customization_phone_mascot.webp new file mode 100644 index 0000000000..c9efa6c9ee Binary files /dev/null and b/app/src/main/res/drawable-nodpi/full_customization_phone_mascot.webp differ diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index fde1368fc1..0000000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/firebase_auth.png b/app/src/main/res/drawable-xhdpi/firebase_auth.png deleted file mode 100644 index 8a93e39a6a..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/firebase_auth.webp b/app/src/main/res/drawable-xhdpi/firebase_auth.webp new file mode 100644 index 0000000000..1bfda9b100 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/firebase_auth.png b/app/src/main/res/drawable-xxhdpi/firebase_auth.png deleted file mode 100644 index c01b18b144..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/firebase_auth.webp b/app/src/main/res/drawable-xxhdpi/firebase_auth.webp new file mode 100644 index 0000000000..600f00a5d1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/firebase_auth.png b/app/src/main/res/drawable-xxxhdpi/firebase_auth.png deleted file mode 100644 index 221da4d3ae..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/firebase_auth.webp b/app/src/main/res/drawable-xxxhdpi/firebase_auth.webp new file mode 100644 index 0000000000..0336659595 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 1e4408cae4..0000000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/font/bagel_fat_one_regular.ttf b/app/src/main/res/font/bagel_fat_one_regular.ttf new file mode 100644 index 0000000000..9de4a2f786 Binary files /dev/null and b/app/src/main/res/font/bagel_fat_one_regular.ttf differ diff --git a/app/src/main/res/font/onest_bold.ttf b/app/src/main/res/font/onest_bold.ttf new file mode 100644 index 0000000000..b0a3dd939e Binary files /dev/null and b/app/src/main/res/font/onest_bold.ttf differ diff --git a/app/src/main/res/font/onest_medium.ttf b/app/src/main/res/font/onest_medium.ttf new file mode 100644 index 0000000000..2ff600481c Binary files /dev/null and b/app/src/main/res/font/onest_medium.ttf differ diff --git a/app/src/main/res/font/onest_regular.ttf b/app/src/main/res/font/onest_regular.ttf new file mode 100644 index 0000000000..dec9f7a23b Binary files /dev/null and b/app/src/main/res/font/onest_regular.ttf differ diff --git a/app/src/main/res/font/onest_semibold.ttf b/app/src/main/res/font/onest_semibold.ttf new file mode 100644 index 0000000000..c7e8a3d2e9 Binary files /dev/null and b/app/src/main/res/font/onest_semibold.ttf differ diff --git a/app/src/main/res/font/roboto_bold.ttf b/app/src/main/res/font/roboto_bold.ttf new file mode 100644 index 0000000000..651618564b Binary files /dev/null and b/app/src/main/res/font/roboto_bold.ttf differ diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000000..bc5b170260 Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/font/roboto_regular.ttf b/app/src/main/res/font/roboto_regular.ttf new file mode 100644 index 0000000000..3db0d1fb08 Binary files /dev/null and b/app/src/main/res/font/roboto_regular.ttf differ diff --git a/app/src/main/res/font/roboto_semibold.ttf b/app/src/main/res/font/roboto_semibold.ttf new file mode 100644 index 0000000000..7a8ef87d58 Binary files /dev/null and b/app/src/main/res/font/roboto_semibold.ttf differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127d32..0000000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0b680c7f1..3d815a6311 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,10 +1,19 @@ - + FirebaseUI Demo CHANGE-HERE - - APP-ID - fbAPP-ID - CHANGE-HERE - \ No newline at end of file + + %1$s — score: %2$d + + + APP-ID + fbAPP-ID + CHANGE-HERE + diff --git a/auth/README.md b/auth/README.md index be8fb1fa7e..613569d90e 100644 --- a/auth/README.md +++ b/auth/README.md @@ -2223,6 +2223,33 @@ Or override individual strings in your `strings.xml`: ``` +### Error message resolution + +Error text is chosen when the exception is built, not when the dialog renders it, and it resolves in this order: + +1. **The type-level hook**, one `fui_error_*` resource per exception type. These ship **deliberately blank**, and a blank value means "skip me" rather than "show nothing". +2. **A per-code string**, selected from the Firebase error code, so a mistyped SMS code and a wrong password no longer produce the same sentence. +3. **The Firebase SDK's own message**, English only, reached only for codes the library does not map. + +Setting a type-level hook therefore overrides *every* code of that type at once. That is occasionally what you want — uniform copy resists account enumeration, since distinguishing "no such account" from "wrong password" tells an attacker which addresses are registered — but it costs you the specific per-code messages: + +```xml + + + Those sign-in details aren\'t correct. + +``` + +Developer misconfiguration is handled separately. `AuthException.MisconfigurationException` carries generic translated copy on `message`, and keeps Firebase's diagnostic on `cause` so it reaches your logs without reaching your users: + +```kotlin +is AuthException.MisconfigurationException -> { + Log.e(TAG, "Check the Firebase console", exception.cause) +} +``` + +One limit worth knowing: `FirebaseAuthUI.signOut`, `withReauth` and `delete` take a `Context` and no configuration, so their messages resolve against that `Context` rather than a `stringProvider` or `locale` you configured. Pass a locale-aware `Context` if that matters. + ## Error Handling FirebaseUI provides a comprehensive exception hierarchy: diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 76d2d5ab92..0bc26ceb51 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -37,14 +37,6 @@ android { } lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - // Module specific disable += mutableSetOf( "UnusedQuantity", @@ -64,12 +56,11 @@ android { "LogConditional" ) - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - // Pre-existing debt only: 168 localization findings (CPRN-432). Every entry is - // suppressed; new ones still fail. + // Pre-existing debt only: 43 MissingTranslation findings (CPRN-432). Every entry + // is suppressed; new ones still fail. They are strings the Compose rewrite added to + // values/strings.xml and never sent for translation, real UI copy in 50 languages, + // so they clear when translations land, not by editing anything here. Delete this + // file once they do. baseline = file("lint-baseline.xml") } diff --git a/auth/lint-baseline.xml b/auth/lint-baseline.xml index b9c0976fa1..451cd3122d 100644 --- a/auth/lint-baseline.xml +++ b/auth/lint-baseline.xml @@ -1,341 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -803,1330 +468,10 @@ message=""fui_error_user_disabled" is not translated in "de" (German), "hi" (Hindi), "ln" (Lingala), "pt" (Portuguese), "fil" (Filipino; Pilipino), "lt" (Lithuanian), "gsw" (Swiss German; Alemannic; Alsatian), "hr" (Croatian), "lv" (Latvian), "hu" (Hungarian), "uk" (Ukrainian), "ur" (Urdu), "mo", "in" (Indonesian), "mr" (Marathi), "ms" (Malay), "el" (Greek), "en" (English), "it" (Italian), "es" (Spanish), "iw" (Hebrew), "zh" (Chinese), "ar" (Arabic), "vi" (Vietnamese), "nb" (Norwegian Bokmål), "ja" (Japanese), "fa" (Persian), "ro" (Romanian), "nl" (Dutch), "no" (Norwegian), "fi" (Finnish), "ru" (Russian), "bg" (Bulgarian), "bn" (Bangla), "fr" (French), "sk" (Slovak), "sl" (Slovenian), "ca" (Catalan), "sr" (Serbian), "kn" (Kannada), "sv" (Swedish), "ko" (Korean), "ta" (Tamil), "gu" (Gujarati), "cs" (Czech), "th" (Thai), "tl" (Tagalog), "pl" (Polish), "da" (Danish), "tr" (Turkish)" errorLine1=" <string name="fui_error_user_disabled" translation_description="Error when a user account has been disabled by an administrator. Override to show a custom message.">User account has been disabled</string>" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt index 6d09582973..eefefb00b6 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -19,7 +19,9 @@ import com.firebase.ui.auth.AuthException.Companion.from import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.google.firebase.FirebaseException +import com.google.firebase.FirebaseTooManyRequestsException import com.google.firebase.auth.AuthCredential +import com.google.firebase.auth.FirebaseAuthActionCodeException import com.google.firebase.auth.FirebaseAuthException import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.FirebaseAuthInvalidUserException @@ -27,6 +29,7 @@ import com.google.firebase.auth.FirebaseAuthMultiFactorException import com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException import com.google.firebase.auth.FirebaseAuthUserCollisionException import com.google.firebase.auth.FirebaseAuthWeakPasswordException +import java.util.Locale /** * Abstract base class representing all possible authentication exceptions in Firebase Auth UI. @@ -93,6 +96,26 @@ abstract class AuthException( cause: Throwable? = null ) : AuthException(message, cause) + /** + * The account exists, but the sign-in method the user just attempted is not available on it. + * + * The attempt was well-formed and the backend answered definitively that this method cannot + * be used for this account, so the error is not recoverable: the error dialog offers no retry + * and the copy points the user at a different way to sign in. + * + * The type is general, but the copy is not. `ERROR_PASSKEY_ENROLLMENT_NOT_FOUND` is the only + * code routed here today and the dialog's fallback for a blank message is `errorPasskeyNotFound`. + * Routing a second code here means giving it its own string and making that fallback a + * per-method choice. + * + * @property message The detailed error message + * @property cause The underlying [Throwable] that caused this exception + */ + class SignInMethodUnavailableException( + message: String, + cause: Throwable? = null + ) : AuthException(message, cause) + /** * The user account does not exist. * @@ -129,11 +152,16 @@ abstract class AuthException( * This exception is thrown when GIdP password policy enforcement is enabled and the supplied * password fails one or more configured constraints (e.g. minimum length, missing uppercase). * - * [message] is a newline-separated, human-readable description of each failing constraint - * as returned by the server, suitable for direct display in the UI. + * [message] is a newline-separated, human-readable description of each failing constraint, + * suitable for direct display in the UI. Built by [from], each constraint is translated copy + * where the library recognises the server's sentence, and the server's own English where it + * does not. * - * @property message Human-readable description of the failing constraints - * @property failingRequirements The individual constraint strings from the server + * [failingRequirements] keeps the **raw** server sentences, untranslated, for hosts that + * render the constraints themselves rather than showing [message]. + * + * @property message Translated description of the failing constraints + * @property failingRequirements The raw, untranslated constraint strings from the server * @property cause The underlying [Throwable] that caused this exception */ class PasswordPolicyViolationException( @@ -256,6 +284,27 @@ abstract class AuthException( cause: Throwable? = null ) : AuthException(message, cause) + /** + * The Firebase project or the app is not set up for the operation that was attempted. + * + * Examples are a sign-in provider left disabled in the Firebase console, an unauthorized + * continue-URL domain, a missing SHA-1 certificate hash, and the reCAPTCHA and tenant + * families. None of these are anything the user can act on. + * + * [message] is generic translated copy, safe to render anywhere — the error dialog, an inline + * error on a screen, or a host's own `onSignInFailure`. The raw Firebase SDK diagnostic is + * untranslated but names the actual misconfiguration, so [from] keeps it on [cause] (the + * original [com.google.firebase.auth.FirebaseAuthException]): it stays in the stack trace and + * is reachable as `exception.cause?.message`. + * + * @property message Generic translated copy, safe to display + * @property cause The original Firebase exception, carrying the raw diagnostic for logs + */ + class MisconfigurationException( + message: String, + cause: Throwable? = null + ) : AuthException(message, cause) + /** * An unknown or unhandled error occurred. * @@ -359,14 +408,24 @@ abstract class AuthException( * This method maps known Firebase exception types to their corresponding [AuthException] * subtypes, providing a consistent exception hierarchy for error handling. * - * **Mapping:** - * - [FirebaseException] → [NetworkException] (for network-related errors) - * - [FirebaseAuthInvalidCredentialsException] → [InvalidCredentialsException] + * **Mapping**, in dispatch order. Several of these types extend one another, so the order + * is load-bearing rather than cosmetic: + * - [FirebaseAuthWeakPasswordException] → [WeakPasswordException], or + * [PasswordPolicyViolationException] when the diagnostic carries a GIdP password-policy + * rejection + * - [FirebaseAuthInvalidCredentialsException] → [InvalidCredentialsException], with the + * message selected by `errorCode`; the `errorCode`s in that family that are developer + * setup faults rather than user error (custom token, OIDC nonce, authenticator + * response) → [MisconfigurationException] * - [FirebaseAuthInvalidUserException] → [UserNotFoundException] - * - [FirebaseAuthWeakPasswordException] → [WeakPasswordException] + * - [FirebaseAuthActionCodeException] → [InvalidCredentialsException] * - [FirebaseAuthUserCollisionException] → [EmailAlreadyInUseException] - * - [FirebaseAuthException] with ERROR_TOO_MANY_REQUESTS → [TooManyRequestsException] * - [FirebaseAuthMultiFactorException] → [MfaRequiredException] + * - [FirebaseAuthRecentLoginRequiredException] → [InvalidCredentialsException] + * - [FirebaseAuthException] with a developer-setup `errorCode` → [MisconfigurationException] + * - [FirebaseTooManyRequestsException] → [TooManyRequestsException] + * - [FirebaseException] → [NetworkException] (for network-related errors), or + * [PasswordPolicyViolationException] when the message carries a policy rejection * - Other exceptions → [UnknownException] * * **Example:** @@ -379,13 +438,51 @@ abstract class AuthException( * } * ``` * + * Messages are resolved against [context]'s own configuration, so this overload honours + * neither a custom [AuthUIStringProvider] nor the `locale` a host configured. Prefer the + * [AuthUIStringProvider] overload wherever one is reachable, which inside an auth flow it + * always is, as `config.stringProvider`. This overload exists for the entry points that + * genuinely have no configuration to draw on, such as [FirebaseAuthUI.signOut], + * [FirebaseAuthUI.withReauth] and [FirebaseAuthUI.delete]. + * * @param firebaseException The Firebase exception to convert + * @param context Used to build a [DefaultAuthUIStringProvider] for the error messages * @return An appropriate [AuthException] subtype */ @JvmStatic fun from(firebaseException: Exception, context: Context): AuthException = from(firebaseException, DefaultAuthUIStringProvider(context)) + /** + * Creates an [AuthException] from [firebaseException], taking message text from + * [stringProvider] so it honours the host's configured strings and locale. + * + * This is the preferred overload; see the [Context] one above for the exception mapping + * table and an example. + * + * Given a non-null [stringProvider], the `message` on an exception returned by **this + * method** is library-owned translated copy, so it is safe to render directly. Each branch + * resolves in this order: the blank-able hook scoped to the exception type, then the + * string for the specific Firebase `errorCode`, then the corresponding generic recovery + * message, and only then the Firebase SDK's own untranslated message. + * [MisconfigurationException] never uses the SDK message at all — the raw diagnostic lives + * on `cause`. A `null` [stringProvider] has nothing to resolve against and falls back to + * the SDK message everywhere except [MisconfigurationException]. + * [PasswordPolicyViolationException] is partial by design: each requirement sentence the + * backend returns is translated when it is recognised and kept verbatim when it is not. + * + * The guarantee covers `from()` only. Subtypes constructed directly carry whatever + * `message` their caller passed, and the email-link subtypes + * ([InvalidEmailLinkException], [EmailLinkWrongDeviceException], + * [EmailLinkCrossDeviceLinkingException], [EmailLinkPromptForEmailException], + * [EmailLinkDifferentAnonymousUserException], [EmailMismatchException]) bake English into + * their own constructors. `getRecoveryMessage` keeps that out of the error dialog by + * resolving those types through [AuthUIStringProvider] instead of reading `message`. + * + * @param firebaseException The Firebase exception to convert + * @param stringProvider Supplies localized message text; pass `config.stringProvider` + * @return An appropriate [AuthException] subtype + */ @JvmStatic @JvmOverloads fun from(firebaseException: Exception, stringProvider: AuthUIStringProvider? = null): AuthException { @@ -399,18 +496,11 @@ abstract class AuthException( is FirebaseAuthWeakPasswordException -> { val sourceText = firebaseException.reason ?: firebaseException.message ?: "" if (sourceText.contains("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true)) { - val requirements = parsePasswordPolicyRequirements(sourceText) - PasswordPolicyViolationException( - message = requirements.joinToString("\n").ifEmpty { - stringProvider?.errorWeakPasswordGeneric.nonEmpty() - ?: "Password does not meet policy requirements" - }, - failingRequirements = requirements, - cause = firebaseException - ) + passwordPolicyViolation(sourceText, firebaseException, stringProvider) } else { WeakPasswordException( message = stringProvider?.errorWeakPasswordGeneric.nonEmpty() + ?: stringProvider?.weakPasswordRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "Password is too weak", cause = firebaseException, @@ -420,18 +510,168 @@ abstract class AuthException( } is FirebaseAuthInvalidCredentialsException -> { - InvalidCredentialsException( - message = stringProvider?.errorInvalidCredentials.nonEmpty() - ?: firebaseException.message - ?: "Invalid credentials provided", - cause = firebaseException - ) + // `errorInvalidCredentials` is the blank-able hook for the whole exception + // type, so it stays ahead of the per-code string in every branch. + val typeLevel = stringProvider?.errorInvalidCredentials.nonEmpty() + when (firebaseException.errorCode) { + // Under email enumeration protection the backend merges "wrong password" + // and "no such account" into this one code, so the copy cannot claim the + // password specifically. + "ERROR_INVALID_CREDENTIAL" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.errorIncorrectEmailOrPassword.nonEmpty() + ?: firebaseException.message + ?: "That email or password isn't correct", + cause = firebaseException + ) + + "ERROR_WRONG_PASSWORD" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.invalidPassword.nonEmpty() + ?: firebaseException.message + ?: "Incorrect password.", + cause = firebaseException + ) + + "ERROR_INVALID_EMAIL" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.invalidEmailAddress.nonEmpty() + ?: firebaseException.message + ?: "That email address isn't correct", + cause = firebaseException + ) + + "ERROR_MISSING_EMAIL" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.missingEmailAddress.nonEmpty() + ?: firebaseException.message + ?: "Enter your email address to continue", + cause = firebaseException + ) + + "ERROR_MISSING_PASSWORD", + "ERROR_MISSING_VERIFICATION_CODE" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.requiredField.nonEmpty() + ?: firebaseException.message + ?: "You can't leave this empty.", + cause = firebaseException + ) + + "ERROR_INVALID_PHONE_NUMBER" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.invalidPhoneNumber.nonEmpty() + ?: firebaseException.message + ?: "Enter a valid phone number", + cause = firebaseException + ) + + "ERROR_MISSING_PHONE_NUMBER" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.missingPhoneNumber.nonEmpty() + ?: firebaseException.message + ?: "You can't leave this empty.", + cause = firebaseException + ) + + "ERROR_INVALID_VERIFICATION_CODE" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.invalidVerificationCode.nonEmpty() + ?: firebaseException.message + ?: "Wrong code. Try again.", + cause = firebaseException + ) + + "ERROR_SESSION_EXPIRED" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.errorSessionExpired.nonEmpty() + ?: firebaseException.message + ?: "This code is no longer valid", + cause = firebaseException + ) + + "ERROR_INVALID_VERIFICATION_ID", + "ERROR_MISSING_VERIFICATION_ID" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.errorInvalidVerificationId.nonEmpty() + ?: firebaseException.message + ?: "That verification session is no longer valid. Request a new code.", + cause = firebaseException + ) + + "ERROR_RETRY_PHONE_AUTH" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.errorRetryPhoneAuth.nonEmpty() + ?: firebaseException.message + ?: "Phone verification didn't complete. Try again.", + cause = firebaseException + ) + + "ERROR_USER_MISMATCH" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.errorUserMismatch.nonEmpty() + ?: firebaseException.message + ?: "Those credentials belong to a different account.", + cause = firebaseException + ) + + "ERROR_PHONE_NUMBER_NOT_FOUND", + "ERROR_MULTI_FACTOR_INFO_NOT_FOUND", + "ERROR_MISSING_MULTI_FACTOR_INFO" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.errorPhoneNumberNotEnrolled.nonEmpty() + ?: firebaseException.message + ?: "That phone number isn't set up for verification on this account.", + cause = firebaseException + ) + + "ERROR_INVALID_MULTI_FACTOR_SESSION", + "ERROR_MISSING_MULTI_FACTOR_SESSION" -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.errorMultiFactorSessionExpired.nonEmpty() + ?: firebaseException.message + ?: "Your sign-in session expired. Sign in again to continue.", + cause = firebaseException + ) + + // Custom tokens are minted by the developer's own backend; the SDK's + // diagnostic names the setup problem and means nothing to the user, so it + // stays on `cause` while `message` carries renderable generic copy. + "ERROR_INVALID_CUSTOM_TOKEN", + "ERROR_CUSTOM_TOKEN_MISMATCH", + // Same shape: the app built the federated request wrong. + "ERROR_MISSING_OR_INVALID_NONCE", + "ERROR_INVALID_AUTHENTICATOR_RESPONSE" -> MisconfigurationException( + message = stringProvider?.unknownErrorRecoveryMessage.nonEmpty() + ?: "An unknown error occurred.", + cause = firebaseException + ) + + // Not InvalidCredentialsException, so its `typeLevel` hook is skipped too. + "ERROR_PASSKEY_ENROLLMENT_NOT_FOUND" -> SignInMethodUnavailableException( + message = stringProvider?.errorPasskeyNotFound.nonEmpty() + ?: firebaseException.message + ?: "We couldn't find a passkey for this account. " + + "Sign in another way.", + cause = firebaseException + ) + + // Unrecognised codes stay recoverable, but the copy must stay generic. + else -> InvalidCredentialsException( + message = typeLevel + ?: stringProvider?.unknownErrorRecoveryMessage.nonEmpty() + ?: firebaseException.message + ?: "Invalid credentials provided", + cause = firebaseException + ) + } } is FirebaseAuthInvalidUserException -> { when (firebaseException.errorCode) { "ERROR_USER_NOT_FOUND" -> UserNotFoundException( message = stringProvider?.errorUserNotFound.nonEmpty() + ?: stringProvider?.userNotFoundRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "User not found", cause = firebaseException @@ -444,8 +684,18 @@ abstract class AuthException( cause = firebaseException ) + "ERROR_INVALID_USER_TOKEN", + "ERROR_USER_TOKEN_EXPIRED" -> InvalidCredentialsException( + message = stringProvider?.errorInvalidCredentials.nonEmpty() + ?: stringProvider?.errorMultiFactorSessionExpired.nonEmpty() + ?: firebaseException.message + ?: "Your sign-in session expired. Sign in again to continue.", + cause = firebaseException + ) + else -> UserNotFoundException( message = stringProvider?.errorUserAccountGeneric.nonEmpty() + ?: stringProvider?.userNotFoundRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "User account error", cause = firebaseException @@ -453,10 +703,36 @@ abstract class AuthException( } } + // Must precede the plain FirebaseAuthException arm, which it extends. + is FirebaseAuthActionCodeException -> { + when (firebaseException.errorCode) { + // The type-level hook is the one scoped to the exception this produces — + // `errorInvalidCredentials`. Using `errorUnknownAuth` here would let a + // host customising the unknown-error copy silently lose this string. + "ERROR_EXPIRED_ACTION_CODE", + "ERROR_INVALID_ACTION_CODE" -> InvalidCredentialsException( + message = stringProvider?.errorInvalidCredentials.nonEmpty() + ?: stringProvider?.errorActionCodeInvalid.nonEmpty() + ?: firebaseException.message + ?: "That link is no longer valid. Request a new one.", + cause = firebaseException + ) + + else -> UnknownException( + message = stringProvider?.errorUnknownAuth.nonEmpty() + ?: stringProvider?.unknownErrorRecoveryMessage.nonEmpty() + ?: firebaseException.message + ?: "An unknown authentication error occurred", + cause = firebaseException + ) + } + } + is FirebaseAuthUserCollisionException -> { when (firebaseException.errorCode) { "ERROR_EMAIL_ALREADY_IN_USE" -> EmailAlreadyInUseException( message = stringProvider?.errorEmailAlreadyInUse.nonEmpty() + ?: stringProvider?.emailAlreadyInUseRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "Email address is already in use", cause = firebaseException, @@ -465,6 +741,7 @@ abstract class AuthException( "ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL" -> AccountLinkingRequiredException( message = stringProvider?.errorAccountExistsDifferentCredential.nonEmpty() + ?: stringProvider?.accountLinkingRequiredRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "Account already exists with different credentials", cause = firebaseException @@ -472,6 +749,7 @@ abstract class AuthException( "ERROR_CREDENTIAL_ALREADY_IN_USE" -> AccountLinkingRequiredException( message = stringProvider?.errorCredentialAlreadyInUse.nonEmpty() + ?: stringProvider?.accountLinkingRequiredRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "Credential is already associated with a different user account", cause = firebaseException @@ -479,6 +757,7 @@ abstract class AuthException( else -> AccountLinkingRequiredException( message = stringProvider?.errorAccountCollisionGeneric.nonEmpty() + ?: stringProvider?.accountLinkingRequiredRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "Account collision error", cause = firebaseException @@ -489,6 +768,7 @@ abstract class AuthException( is FirebaseAuthMultiFactorException -> { MfaRequiredException( message = stringProvider?.errorMfaRequiredFallback.nonEmpty() + ?: stringProvider?.mfaRequiredRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "Multi-factor authentication required", cause = firebaseException @@ -496,8 +776,10 @@ abstract class AuthException( } is FirebaseAuthRecentLoginRequiredException -> { + // `errorRecentLoginRequired` ships blank; the MFA string says the same thing. InvalidCredentialsException( message = stringProvider?.errorRecentLoginRequired.nonEmpty() + ?: stringProvider?.mfaErrorRecentLoginRequired.nonEmpty() ?: firebaseException.message ?: "Recent login required for this operation", cause = firebaseException @@ -506,23 +788,97 @@ abstract class AuthException( is FirebaseAuthException -> { when (firebaseException.errorCode) { - "ERROR_TOO_MANY_REQUESTS" -> TooManyRequestsException( - message = stringProvider?.errorTooManyRequests.nonEmpty() + // FirebaseAuthWebException code for backing out of the OAuth custom tab, + // and the Credential Manager / Play services equivalent. + "ERROR_WEB_CONTEXT_CANCELED", + "ERROR_USER_CANCELLED" -> AuthCancelledException( + message = stringProvider?.errorAuthCancelled.nonEmpty() + ?: stringProvider?.authCancelledRecoveryMessage.nonEmpty() + ?: firebaseException.message + ?: "Authentication was cancelled", + cause = firebaseException + ) + + // These three produce InvalidCredentialsException, so the type-level hook + // is `errorInvalidCredentials`. `errorUnknownAuth` would let a host that + // customises only the unknown-error copy lose all three specific strings. + "ERROR_UNVERIFIED_EMAIL" -> InvalidCredentialsException( + message = stringProvider?.errorInvalidCredentials.nonEmpty() + ?: stringProvider?.errorUnverifiedEmail.nonEmpty() ?: firebaseException.message - ?: "Too many requests. Please try again later", + ?: "Verify your email address before you continue.", cause = firebaseException ) - // FirebaseAuthWebException code for backing out of the OAuth custom tab - "ERROR_WEB_CONTEXT_CANCELED" -> AuthCancelledException( - message = stringProvider?.errorAuthCancelled.nonEmpty() + "ERROR_SECOND_FACTOR_ALREADY_ENROLLED" -> InvalidCredentialsException( + message = stringProvider?.errorInvalidCredentials.nonEmpty() + ?: stringProvider?.errorSecondFactorAlreadyEnrolled.nonEmpty() ?: firebaseException.message - ?: "Authentication was cancelled", + ?: "That verification method is already set up on this account.", + cause = firebaseException + ) + + "ERROR_MAXIMUM_SECOND_FACTOR_COUNT_EXCEEDED" -> InvalidCredentialsException( + message = stringProvider?.errorInvalidCredentials.nonEmpty() + ?: stringProvider?.errorMaximumSecondFactorCountExceeded.nonEmpty() + ?: firebaseException.message + ?: "You've reached the limit for verification methods on this account.", + cause = firebaseException + ) + + // Developer setup problems. The user can do nothing about any of them, so + // `message` carries generic translated copy and the raw Firebase + // diagnostic is kept on `cause` for logs. INTERNAL_ERROR and + // ERROR_WEB_INTERNAL_ERROR are deliberately absent — they are backend + // faults, not configuration. + "ERROR_OPERATION_NOT_ALLOWED", + "ERROR_APP_NOT_AUTHORIZED", + "ERROR_UNAUTHORIZED_DOMAIN", + "ERROR_MISSING_CONTINUE_URI", + "ERROR_INVALID_CERT_HASH", + "ERROR_DYNAMIC_LINK_NOT_ACTIVATED", + "ERROR_INVALID_DYNAMIC_LINK_DOMAIN", + "ERROR_INVALID_HOSTING_LINK_DOMAIN", + "ERROR_INVALID_PROVIDER_ID", + "ERROR_ADMIN_RESTRICTED_OPERATION", + "ERROR_UNSUPPORTED_FIRST_FACTOR", + "ERROR_UNSUPPORTED_PASSTHROUGH_OPERATION", + "ERROR_INVALID_REQ_TYPE", + "ERROR_WEB_CONTEXT_ALREADY_PRESENTED", + // Tenant family + "ERROR_INVALID_TENANT_ID", + "ERROR_TENANT_ID_MISMATCH", + "ERROR_UNSUPPORTED_TENANT_OPERATION", + // reCAPTCHA / app verification family + "ERROR_RECAPTCHA_NOT_ENABLED", + "ERROR_CAPTCHA_CHECK_FAILED", + "ERROR_MISSING_RECAPTCHA_TOKEN", + "ERROR_INVALID_RECAPTCHA_TOKEN", + "ERROR_INVALID_RECAPTCHA_ACTION", + "ERROR_MISSING_RECAPTCHA_VERSION", + "ERROR_INVALID_RECAPTCHA_VERSION", + "ERROR_MISSING_CLIENT_TYPE", + "ERROR_MISSING_CLIENT_IDENTIFIER", + "ERROR_ALTERNATE_CLIENT_IDENTIFIER_REQUIRED", + // Email-template settings in the Firebase console. These arrive as + // FirebaseAuthEmailException, which extends FirebaseAuthException + // directly and so lands in this arm. + "ERROR_INVALID_MESSAGE_PAYLOAD", + "ERROR_INVALID_SENDER", + "ERROR_INVALID_RECIPIENT_EMAIL", + // Host integration and project quota. ERROR_MISSING_ACTIVITY ships: it is + // declared on the Recaptcha-activity exception, not in the SDK code table. + "ERROR_MISSING_ACTIVITY", + "ERROR_WEB_STORAGE_UNSUPPORTED", + "ERROR_QUOTA_EXCEEDED" -> MisconfigurationException( + message = stringProvider?.unknownErrorRecoveryMessage.nonEmpty() + ?: "An unknown error occurred.", cause = firebaseException ) else -> UnknownException( message = stringProvider?.errorUnknownAuth.nonEmpty() + ?: stringProvider?.unknownErrorRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "An unknown authentication error occurred", cause = firebaseException @@ -530,21 +886,27 @@ abstract class AuthException( } } + // Rate limiting arrives as a plain FirebaseTooManyRequestsException, which is NOT a + // FirebaseAuthException and carries no error code. Without this arm it falls to the + // FirebaseException branch below and a throttled user is told they are offline. + is FirebaseTooManyRequestsException -> { + TooManyRequestsException( + message = stringProvider?.errorTooManyRequests.nonEmpty() + ?: stringProvider?.tooManyRequestsRecoveryMessage.nonEmpty() + ?: firebaseException.message + ?: "Too many requests. Please try again later", + cause = firebaseException + ) + } + is FirebaseException -> { val msg = firebaseException.message ?: "" if (msg.contains("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true)) { - val requirements = parsePasswordPolicyRequirements(msg) - PasswordPolicyViolationException( - message = requirements.joinToString("\n").ifEmpty { - stringProvider?.errorWeakPasswordGeneric.nonEmpty() - ?: "Password does not meet policy requirements" - }, - failingRequirements = requirements, - cause = firebaseException - ) + passwordPolicyViolation(msg, firebaseException, stringProvider) } else { NetworkException( message = stringProvider?.errorNetworkGeneric.nonEmpty() + ?: stringProvider?.networkErrorRecoveryMessage.nonEmpty() ?: msg.ifEmpty { "Network error occurred" }, cause = firebaseException ) @@ -557,6 +919,7 @@ abstract class AuthException( ) { AuthCancelledException( message = stringProvider?.errorAuthCancelled.nonEmpty() + ?: stringProvider?.authCancelledRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "Authentication was cancelled", cause = firebaseException @@ -564,6 +927,7 @@ abstract class AuthException( } else { UnknownException( message = stringProvider?.errorUnknownAuth.nonEmpty() + ?: stringProvider?.unknownErrorRecoveryMessage.nonEmpty() ?: firebaseException.message ?: "An unknown error occurred", cause = firebaseException @@ -575,6 +939,89 @@ abstract class AuthException( private fun String?.nonEmpty(): String? = this?.ifEmpty { null } + /** + * Builds the [PasswordPolicyViolationException] for a GIdP password-policy rejection: + * `message` is localized one requirement sentence at a time, while `failingRequirements` + * keeps the backend's raw sentences. + */ + private fun passwordPolicyViolation( + sourceText: String, + cause: Exception, + stringProvider: AuthUIStringProvider? + ): PasswordPolicyViolationException { + val requirements = parsePasswordPolicyRequirements(sourceText) + return PasswordPolicyViolationException( + message = requirements + .joinToString("\n") { localizePasswordRequirement(it, stringProvider) ?: it } + .ifEmpty { + // `errorWeakPasswordGeneric` is the host's hook and ships blank. + stringProvider?.errorWeakPasswordGeneric.nonEmpty() + ?: stringProvider?.errorPasswordPolicyGeneric.nonEmpty() + ?: "Password does not meet policy requirements" + }, + failingRequirements = requirements, + cause = cause + ) + } + + /** + * Translates one GIdP password-policy requirement sentence, or returns `null` when the + * sentence is not recognised. + * + * The sentences are the backend's own English, e.g. "Password must contain at least 10 + * characters". On `null` the caller keeps that sentence verbatim, so a reworded or newly + * added requirement degrades to untranslated English rather than to a wrong message. + */ + private fun localizePasswordRequirement( + requirement: String, + stringProvider: AuthUIStringProvider? + ): String? { + if (stringProvider == null) return null + // GIdP writes "upper case" and "lower case" as two words; one word is accepted too. + val text = requirement.lowercase(Locale.ROOT) + return when { + text.contains("upper case") || text.contains("uppercase") -> + stringProvider.passwordMissingUppercase.nonEmpty() + + text.contains("lower case") || text.contains("lowercase") -> + stringProvider.passwordMissingLowercase.nonEmpty() + + // "numeric" is a substring of "non-alphanumeric": the guard keeps the two arms + // disjoint regardless of the order they are tested in. + text.contains("numeric") && !text.contains("non-alphanumeric") -> + stringProvider.passwordMissingDigit.nonEmpty() + + // Unverified wording: the probe project had special characters disabled. + text.contains("non-alphanumeric") || text.contains("special character") -> + stringProvider.passwordMissingSpecialCharacter.nonEmpty() + + // The number is the project's own configured minimum, so it is read out of the + // sentence rather than assumed. + text.contains("at least") -> + firstNumberIn(requirement)?.let { + stringProvider.passwordTooShort(it).nonEmpty() + } + + // "fewer than N" is exclusive, so the maximum passwordTooLong states is N - 1. + // Unverified wording: the probe project left the maximum at its 4096 default. + text.contains("fewer than") -> + firstNumberIn(requirement)?.let { + stringProvider.passwordTooLong(it - 1).nonEmpty() + } + + // "at most N" and "no more than N" are inclusive, so N is the maximum as written. + text.contains("at most") || text.contains("no more than") -> + firstNumberIn(requirement)?.let { + stringProvider.passwordTooLong(it).nonEmpty() + } + + else -> null + } + } + + private fun firstNumberIn(text: String): Int? = + Regex("\\d+").find(text)?.value?.toIntOrNull() + // Finds the [...] content that immediately follows PASSWORD_DOES_NOT_MEET_REQUIREMENTS // in both FirebaseException and FirebaseAuthWeakPasswordException messages. // GIdP returns human-readable requirement strings inside those brackets, e.g. diff --git a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt index 1978eabde0..e321ffcf21 100644 --- a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt @@ -20,10 +20,12 @@ import androidx.annotation.MainThread import androidx.annotation.RestrictTo import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.auth_provider.Provider import com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException import com.firebase.ui.auth.configuration.auth_provider.signOutFromFacebook import com.firebase.ui.auth.configuration.auth_provider.signOutFromGoogle import com.firebase.ui.auth.ui.screens.reauth.toReauthConfiguration +import com.firebase.ui.auth.util.ProviderAvailability import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.auth.AuthResult @@ -400,6 +402,11 @@ class FirebaseAuthUI private constructor( * to reflect the change. The operation is performed asynchronously and will emit * appropriate states during the process. * + * It also clears the session held by any social provider linked to the account, so the next + * sign-in starts clean: Google's saved credential state is cleared, meaning the account picker + * is shown again instead of silently re-selecting the previous account, and any Facebook + * session is logged out. Failures there are logged and do not fail the sign-out. + * * **Example:** * ```kotlin * val authUI = FirebaseAuthUI.getInstance() @@ -431,21 +438,39 @@ class FirebaseAuthUI private constructor( // Update state to loading updateAuthState(AuthState.Loading(context.getString(R.string.fui_loading_signing_out))) + // Capture the linked providers before signing out: `auth.signOut()` clears + // `currentUser`, and `FirebaseUser.providerId` is always "firebase" — the + // per-provider ids live in `providerData`. + val linkedProviderIds = auth.currentUser?.providerData + ?.map { it.providerId } + .orEmpty() + // Sign out from Firebase Auth auth.signOut() - .also { - signOutFromGoogle( - auth = auth, - context = context, - credentialManagerProvider = testCredentialManagerProvider - ?: AuthProvider.Google.DefaultCredentialManagerProvider(), - ) - signOutFromFacebook( - auth = auth, - loginManagerProvider = testLoginManagerProvider - ?: AuthProvider.Facebook.DefaultLoginManagerProvider(), - ) - } + + // Clear the provider-side session for each provider linked to the account. This is + // the linked set rather than the provider used for this session, so it can clear a + // little more than strictly necessary — cheap either way, and it never leaves a + // provider session behind. + if (Provider.GOOGLE.id in linkedProviderIds) { + signOutFromGoogle( + context = context, + credentialManagerProvider = testCredentialManagerProvider + ?: AuthProvider.Google.DefaultCredentialManagerProvider(), + ) + } + // Facebook is a `compileOnly` dependency, so an app that doesn't offer Facebook + // sign-in has no Facebook SDK at runtime — and `providerData` can still carry + // `facebook.com` for an account linked on another platform. Touching the Facebook + // extensions at all links the SDK, so the classpath probe, not the account, decides. + if (Provider.FACEBOOK.id in linkedProviderIds && + ProviderAvailability.IS_FACEBOOK_AVAILABLE + ) { + signOutFromFacebook( + loginManagerProvider = testLoginManagerProvider + ?: AuthProvider.Facebook.DefaultLoginManagerProvider(), + ) + } // Update state to idle (user signed out) updateAuthState(AuthState.Idle) diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt index 3c5f2df498..4f9fe0d891 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProvider+FirebaseAuthUI.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.tasks.await internal fun AuthFlowScope.rememberAnonymousSignInHandler( onSignInFailure: (AuthException) -> Unit = {}, ): () -> Unit { - val context = androidx.compose.ui.platform.LocalContext.current val coroutineScope = rememberCoroutineScope() return { coroutineScope.launch { @@ -33,7 +32,7 @@ internal fun AuthFlowScope.rememberAnonymousSignInHandler( emit(AuthState.Error(e)) if (e !is AuthException.AuthCancelledException) onSignInFailure(e) } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException) } @@ -63,7 +62,7 @@ internal suspend fun AuthFlowScope.signInAnonymously() { emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt index bb1d277926..ea7e7e707a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt @@ -192,7 +192,7 @@ internal suspend fun AuthFlowScope.createOrLinkUserWithEmailAndPassword( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -351,7 +351,7 @@ internal suspend fun AuthFlowScope.signInWithEmailAndPassword( throw e } catch (e: Exception) { val authException = recoverLegacyDifferentSignInMethod(email, e) - ?: AuthException.from(e, context) + ?: AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -365,7 +365,7 @@ private suspend fun AuthFlowScope.recoverLegacyDifferentSignInMethod( return null } - val authException = AuthException.from(cause) + val authException = AuthException.from(cause, config.stringProvider) if (authException !is AuthException.InvalidCredentialsException && authException !is AuthException.UserNotFoundException) { return null @@ -500,7 +500,7 @@ internal suspend fun AuthFlowScope.signInAndLinkWithCredential( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -565,7 +565,7 @@ internal suspend fun AuthFlowScope.sendSignInLinkToEmail( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -702,7 +702,7 @@ internal suspend fun AuthFlowScope.signInWithEmailLink( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -824,7 +824,7 @@ internal suspend fun AuthFlowScope.sendPasswordResetEmail( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt index 0a8a7212be..591ae68b3d 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt @@ -14,7 +14,6 @@ package com.firebase.ui.auth.configuration.auth_provider -import com.google.firebase.auth.FirebaseAuth import android.content.Context import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult @@ -95,7 +94,7 @@ internal fun AuthFlowScope.rememberSignInWithFacebookLauncher( currentScope.emit(AuthState.Error(e)) if (e !is AuthException.AuthCancelledException) currentOnSignInFailure(e) } catch (e: Exception) { - val authException = AuthException.from(e, currentContext) + val authException = AuthException.from(e, currentScope.config.stringProvider) currentScope.emit(AuthState.Error(authException)) if (authException !is AuthException.AuthCancelledException) currentOnSignInFailure(authException) } @@ -108,7 +107,7 @@ internal fun AuthFlowScope.rememberSignInWithFacebookLauncher( override fun onError(error: FacebookException) { Log.e("FacebookAuthProvider", "Error during Facebook sign in", error) - val authException = AuthException.from(error, currentContext) + val authException = AuthException.from(error, currentScope.config.stringProvider) currentScope.emit( AuthState.Error( authException @@ -203,7 +202,7 @@ internal suspend fun AuthFlowScope.signInWithFacebook( emit(AuthState.Error(e)) throw e } catch (e: FacebookException) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } catch (e: CancellationException) { @@ -217,29 +216,28 @@ internal suspend fun AuthFlowScope.signInWithFacebook( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } } /** - * Signs out the current user from Facebook. + * Logs the user out of their Facebook session via Facebook's LoginManager. * - * Invokes Facebook's LoginManager to log out the user from their Facebook session. - * This method silently catches and ignores any exceptions that may occur during the - * logout process to ensure the sign-out flow continues even if Facebook logout fails. + * Best-effort: failures are logged and swallowed so sign-out continues. [LinkageError] is caught + * alongside [Exception] because the Facebook SDK is `compileOnly`, so an absent or mismatched SDK + * surfaces as an error rather than an exception. * - * This is typically called as part of the overall sign-out flow when a user signs out - * from Firebase Authentication. + * The caller decides whether Facebook applies; reaching this function at all links the SDK. */ internal fun signOutFromFacebook( - auth: FirebaseAuth, loginManagerProvider: AuthProvider.Facebook.LoginManagerProvider = AuthProvider.Facebook.DefaultLoginManagerProvider(), ) { try { - if (Provider.fromId(auth.currentUser?.providerId) != Provider.FACEBOOK) return loginManagerProvider.logOut() + } catch (e: LinkageError) { + Log.e("FacebookAuthProvider", "Facebook SDK not available or mismatched", e) } catch (e: Exception) { Log.e("FacebookAuthProvider", "Error during Facebook sign out", e) } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt index 96ffbb79f3..873be28da2 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt @@ -1,6 +1,5 @@ package com.firebase.ui.auth.configuration.auth_provider -import com.google.firebase.auth.FirebaseAuth import android.content.Context import android.util.Log import androidx.compose.runtime.Composable @@ -38,7 +37,7 @@ internal fun AuthFlowScope.rememberGoogleSignInHandler( emit(AuthState.Error(e)) if (e !is AuthException.AuthCancelledException) onSignInFailure(e) } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException) } @@ -73,7 +72,7 @@ internal suspend fun AuthFlowScope.signInWithGoogle( authorizationProvider.authorize(context, requestedScopes) } catch (e: Exception) { // Continue with sign-in even if scope authorization fails - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) } } @@ -193,7 +192,7 @@ internal suspend fun AuthFlowScope.signInWithGoogle( throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -214,19 +213,25 @@ internal suspend fun AuthFlowScope.signInWithGoogle( * **Note:** This does not sign out from Firebase Auth itself. Call [com.firebase.ui.auth.FirebaseAuthUI.signOut] * separately if you need to sign out from Firebase. * + * Callers are responsible for deciding whether Google is involved at all — this function does not + * check the signed-in user, and [com.firebase.ui.auth.FirebaseAuthUI.signOut] has already cleared + * it by the time it calls here. + * * @param context Android context for Credential Manager */ internal suspend fun signOutFromGoogle( - auth: FirebaseAuth, context: Context, credentialManagerProvider: AuthProvider.Google.CredentialManagerProvider = AuthProvider.Google.DefaultCredentialManagerProvider(), ) { try { - if (Provider.fromId(auth.currentUser?.providerId) != Provider.GOOGLE) return credentialManagerProvider.clearCredentialState( context = context, credentialManager = CredentialManager.create(context) ) + } catch (e: CancellationException) { + // Must not be swallowed: this suspends, so cancellation has to reach the caller for + // FirebaseAuthUI.signOut to report it rather than emitting Idle for a half-done sign-out. + throw e } catch (e: Exception) { Log.e("GoogleAuthProvider", "Error during Google sign out", e) } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt index 1b5fd3db8d..86e40f3d11 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt @@ -49,7 +49,7 @@ internal fun AuthFlowScope.rememberOAuthSignInHandler( emit(AuthState.Error(e)) if (e !is AuthException.AuthCancelledException) onSignInFailure(e) } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException) } @@ -191,7 +191,7 @@ internal suspend fun AuthFlowScope.signInWithProvider( throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt index d7c6757a2a..bfbf387747 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt @@ -68,7 +68,7 @@ internal suspend fun AuthFlowScope.verifyPhoneNumber( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -107,7 +107,7 @@ internal suspend fun AuthFlowScope.submitVerificationCode( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } @@ -161,7 +161,7 @@ internal suspend fun AuthFlowScope.signInWithPhoneAuthCredential( emit(AuthState.Error(e)) throw e } catch (e: Exception) { - val authException = AuthException.from(e, context) + val authException = AuthException.from(e, config.stringProvider) emit(AuthState.Error(authException)) throw authException } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt index 7e1c3698d6..58d0e4b79a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt @@ -184,6 +184,9 @@ interface AuthUIStringProvider { /** Error message when password doesn't meet minimum length requirement. Should support string formatting with minimum length parameter. */ fun passwordTooShort(minimumLength: Int): String + /** Error message when the password is longer than the maximum length allowed. Should support string formatting with maximum length parameter. */ + fun passwordTooLong(maximumLength: Int): String + /** Error message when password is missing at least one uppercase letter (A-Z) */ val passwordMissingUppercase: String @@ -628,4 +631,62 @@ interface AuthUIStringProvider { /** Error when authentication is cancelled. Return empty to use the Firebase SDK message. */ val errorAuthCancelled: String + + // ============================================================================================= + // AuthException messages selected by Firebase Auth error code + // + // Every member below has a default so that adding one is not a breaking change, and each + // default delegates to a coarser member rather than returning a hardcoded English literal — + // a host that has implemented this interface itself keeps getting its own translated copy. + // ============================================================================================= + + /** Error when sign-in fails and the server will not say whether the email or the password was wrong. */ + val errorIncorrectEmailOrPassword: String get() = errorInvalidCredentials + + /** Error when the SMS verification session is gone and a new code has to be requested. */ + val errorInvalidVerificationId: String get() = errorInvalidCredentials + + /** Error when phone verification did not complete and has to be retried. */ + val errorRetryPhoneAuth: String get() = errorInvalidCredentials + + /** Error when the supplied credentials belong to a different account than the one being confirmed. */ + val errorUserMismatch: String get() = errorUnknownAuth + + /** Error when the phone number is not set up as a verification method on the account. */ + val errorPhoneNumberNotEnrolled: String get() = errorInvalidCredentials + + /** Error when a sign-in or verification session has expired. */ + val errorSessionExpired: String get() = errorInvalidCredentials + + /** Error when the sign-in session expired part-way through two-step verification. */ + val errorMultiFactorSessionExpired: String get() = errorSessionExpired + + /** Error when an emailed sign-in or password reset link has expired or is malformed. */ + val errorActionCodeInvalid: String get() = errorInvalidCredentials + + /** Error when the account email has to be verified before the operation can continue. */ + val errorUnverifiedEmail: String get() = errorUnknownAuth + + /** Error when adding a verification method that is already set up on the account. */ + val errorSecondFactorAlreadyEnrolled: String get() = errorUnknownAuth + + /** Error when the account already has the maximum number of verification methods. */ + val errorMaximumSecondFactorCountExceeded: String get() = errorUnknownAuth + + /** + * Error when the password fails the project's password policy and the server named no + * individual requirement. When the server does name them, each one is rendered through + * [passwordTooShort], [passwordTooLong], [passwordMissingUppercase], + * [passwordMissingLowercase], [passwordMissingDigit] and [passwordMissingSpecialCharacter] + * instead, and this string is not used. + */ + val errorPasswordPolicyGeneric: String get() = errorWeakPasswordGeneric + + /** + * Error when the account has no passkey enrolled and the user has to sign in another way. + * + * Defaults to [errorUnknownAuth], not [errorInvalidCredentials]: this message is shown for a + * non-recoverable error, so the dialog offers no retry and credential copy would contradict it. + */ + val errorPasskeyNotFound: String get() = errorUnknownAuth } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt index e25776a76b..9279d3c49a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt @@ -26,6 +26,10 @@ class DefaultAuthUIStringProvider( /** * Allows overriding locale. */ + // AppBundleLocaleChanges tells app modules to pair a dynamic locale change with a Play Core + // language download. A library cannot: the bundle configuration and any Play Core dependency + // belong to the app that embeds us, so there is nothing here to fix. + @Suppress("AppBundleLocaleChanges") private val localizedContext = locale?.let { locale -> context.createConfigurationContext( Configuration(context.resources.configuration).apply { @@ -155,6 +159,9 @@ class DefaultAuthUIStringProvider( override fun passwordTooShort(minimumLength: Int): String = localizedContext.getString(R.string.fui_error_password_too_short, minimumLength) + override fun passwordTooLong(maximumLength: Int): String = + localizedContext.getString(R.string.fui_error_password_too_long, maximumLength) + override val passwordMissingUppercase: String get() = localizedContext.getString(R.string.fui_error_password_missing_uppercase) override val passwordMissingLowercase: String @@ -567,4 +574,43 @@ class DefaultAuthUIStringProvider( override val errorAuthCancelled: String get() = localizedContext.getString(R.string.fui_error_auth_cancelled) + + override val errorIncorrectEmailOrPassword: String + get() = localizedContext.getString(R.string.fui_error_incorrect_email_or_password) + + override val errorInvalidVerificationId: String + get() = localizedContext.getString(R.string.fui_error_invalid_verification_id) + + override val errorRetryPhoneAuth: String + get() = localizedContext.getString(R.string.fui_error_retry_phone_auth) + + override val errorUserMismatch: String + get() = localizedContext.getString(R.string.fui_error_user_mismatch) + + override val errorPhoneNumberNotEnrolled: String + get() = localizedContext.getString(R.string.fui_error_phone_number_not_enrolled) + + override val errorSessionExpired: String + get() = localizedContext.getString(R.string.fui_error_session_expired) + + override val errorMultiFactorSessionExpired: String + get() = localizedContext.getString(R.string.fui_error_multi_factor_session_expired) + + override val errorActionCodeInvalid: String + get() = localizedContext.getString(R.string.fui_error_action_code_invalid) + + override val errorUnverifiedEmail: String + get() = localizedContext.getString(R.string.fui_error_unverified_email) + + override val errorSecondFactorAlreadyEnrolled: String + get() = localizedContext.getString(R.string.fui_error_second_factor_already_enrolled) + + override val errorMaximumSecondFactorCountExceeded: String + get() = localizedContext.getString(R.string.fui_error_maximum_second_factor_count_exceeded) + + override val errorPasswordPolicyGeneric: String + get() = localizedContext.getString(R.string.fui_error_password_policy_generic) + + override val errorPasskeyNotFound: String + get() = localizedContext.getString(R.string.fui_error_passkey_not_found) } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt index ddb18c6426..2b4e90a768 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt @@ -143,19 +143,27 @@ internal fun getRecoveryMessage( stringProvider: AuthUIStringProvider ): String { return when (error) { - is AuthException.NetworkException -> stringProvider.networkErrorRecoveryMessage + // AuthException.from already puts generic translated copy on the message and keeps the + // raw diagnostic on the cause, so this arm is belt-and-braces: an instance constructed + // directly with a raw diagnostic still cannot leak it into the dialog. + is AuthException.MisconfigurationException -> stringProvider.unknownErrorRecoveryMessage + is AuthException.NetworkException -> + error.message?.takeIf { it.isNotBlank() } ?: stringProvider.networkErrorRecoveryMessage is AuthException.InvalidCredentialsException -> { - // Use the actual error message from Firebase if available, otherwise fallback to generic message - error.message?.takeIf { it.isNotBlank() && it != "Invalid credentials provided" } + // AuthException.from now picks library-owned copy per Firebase error code, so the + // message is the specific one; the generic string is only the empty-message fallback. + error.message?.takeIf { it.isNotBlank() } ?: stringProvider.invalidCredentialsRecoveryMessage } - is AuthException.UserNotFoundException -> stringProvider.userNotFoundRecoveryMessage + is AuthException.SignInMethodUnavailableException -> + // Passkey-specific fallback behind a general type — see the exception's KDoc. + error.message?.takeIf { it.isNotBlank() } ?: stringProvider.errorPasskeyNotFound + is AuthException.UserNotFoundException -> + error.message?.takeIf { it.isNotBlank() } ?: stringProvider.userNotFoundRecoveryMessage is AuthException.WeakPasswordException -> { - // Include specific reason if available - val baseMessage = stringProvider.weakPasswordRecoveryMessage - error.reason?.let { reason -> - "$baseMessage\n\nReason: $reason" - } ?: baseMessage + // `error.reason` is untranslated SDK text, so it is deliberately not appended. + error.message?.takeIf { it.isNotBlank() } + ?: stringProvider.weakPasswordRecoveryMessage } is AuthException.PasswordPolicyViolationException -> { @@ -165,24 +173,30 @@ internal fun getRecoveryMessage( is AuthException.EmailAlreadyInUseException -> { // Include email if available - val baseMessage = stringProvider.emailAlreadyInUseRecoveryMessage + val baseMessage = error.message?.takeIf { it.isNotBlank() } + ?: stringProvider.emailAlreadyInUseRecoveryMessage error.email?.let { email -> "$baseMessage ($email)" } ?: baseMessage } - is AuthException.TooManyRequestsException -> stringProvider.tooManyRequestsRecoveryMessage + is AuthException.TooManyRequestsException -> + error.message?.takeIf { it.isNotBlank() } + ?: stringProvider.tooManyRequestsRecoveryMessage is AuthException.PhoneVerificationCooldownException -> { // Use the custom message which includes remaining cooldown time - error.message ?: stringProvider.unknownErrorRecoveryMessage + error.message?.takeIf { it.isNotBlank() } ?: stringProvider.unknownErrorRecoveryMessage } - is AuthException.MfaRequiredException -> stringProvider.mfaRequiredRecoveryMessage + is AuthException.MfaRequiredException -> + error.message?.takeIf { it.isNotBlank() } ?: stringProvider.mfaRequiredRecoveryMessage is AuthException.AccountLinkingRequiredException -> { // Use the custom message which includes email and provider details - error.message ?: stringProvider.accountLinkingRequiredRecoveryMessage + error.message?.takeIf { it.isNotBlank() } + ?: stringProvider.accountLinkingRequiredRecoveryMessage } is AuthException.DifferentSignInMethodRequiredException -> { - error.message ?: stringProvider.accountLinkingRequiredRecoveryMessage + error.message?.takeIf { it.isNotBlank() } + ?: stringProvider.accountLinkingRequiredRecoveryMessage } is AuthException.EmailMismatchException -> stringProvider.emailMismatchMessage is AuthException.InvalidEmailLinkException -> stringProvider.emailLinkInvalidLinkMessage @@ -194,7 +208,8 @@ internal fun getRecoveryMessage( val providerName = error.providerName ?: stringProvider.emailProvider stringProvider.emailLinkCrossDeviceLinkingMessage(providerName) } - is AuthException.AuthCancelledException -> stringProvider.authCancelledRecoveryMessage + is AuthException.AuthCancelledException -> + error.message?.takeIf { it.isNotBlank() } ?: stringProvider.authCancelledRecoveryMessage is AuthException.UnknownException -> { // Use custom message if available (e.g., for configuration errors) error.message?.takeIf { it.isNotBlank() } ?: stringProvider.unknownErrorRecoveryMessage @@ -214,6 +229,7 @@ internal fun getRecoveryActionText( error: AuthException, stringProvider: AuthUIStringProvider ): String { + if (!isRecoverable(error)) return stringProvider.dismissAction return when (error) { is AuthException.AuthCancelledException -> stringProvider.continueText is AuthException.EmailAlreadyInUseException -> stringProvider.signInDefault // Use existing "Sign in" text @@ -224,14 +240,11 @@ internal fun getRecoveryActionText( is AuthException.EmailLinkPromptForEmailException -> stringProvider.continueText is AuthException.EmailLinkCrossDeviceLinkingException -> stringProvider.continueText is AuthException.EmailLinkWrongDeviceException -> stringProvider.continueText - is AuthException.EmailLinkDifferentAnonymousUserException -> stringProvider.dismissAction is AuthException.UserNotFoundException -> stringProvider.signupPageTitle // Navigate to sign-up when user not found is AuthException.NetworkException, is AuthException.InvalidCredentialsException, is AuthException.WeakPasswordException, - is AuthException.PasswordPolicyViolationException, - is AuthException.TooManyRequestsException, - is AuthException.PhoneVerificationCooldownException -> stringProvider.retryAction + is AuthException.PasswordPolicyViolationException -> stringProvider.retryAction is AuthException.UnknownException -> stringProvider.retryAction else -> stringProvider.retryAction @@ -262,6 +275,9 @@ internal fun isRecoverable(error: AuthException): Boolean { is AuthException.EmailLinkCrossDeviceLinkingException -> true is AuthException.EmailLinkWrongDeviceException -> true is AuthException.EmailLinkDifferentAnonymousUserException -> false + is AuthException.MisconfigurationException -> false // Retrying cannot fix project setup + // The method is not available on this account; repeating it cannot change that. + is AuthException.SignInMethodUnavailableException -> false is AuthException.UnknownException -> true else -> true } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/components/TopLevelDialogController.kt b/auth/src/main/java/com/firebase/ui/auth/ui/components/TopLevelDialogController.kt index a5b73917e5..89a4a82ed2 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/components/TopLevelDialogController.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/components/TopLevelDialogController.kt @@ -19,10 +19,12 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider /** * CompositionLocal for accessing the top-level dialog controller from any composable. @@ -40,7 +42,7 @@ val LocalTopLevelDialogController = compositionLocalOf AuthState ) { + /** + * Only ever set by the deprecated constructor below. A caller that passed a provider without + * also providing [LocalAuthUIStringProvider] still works, instead of trading a compile-time + * argument for a runtime `error("No AuthUIStringProvider provided")`. + */ + private var explicitStringProvider: AuthUIStringProvider? = null + + @Deprecated( + "The string provider is now read from LocalAuthUIStringProvider at render time.", + ReplaceWith("TopLevelDialogController(currentAuthState)") + ) + constructor( + stringProvider: AuthUIStringProvider, + currentAuthState: () -> AuthState + ) : this(currentAuthState) { + explicitStringProvider = stringProvider + } + private var dialogState by mutableStateOf(null) private val shownErrorStates = mutableSetOf() @@ -125,11 +147,14 @@ class TopLevelDialogController( /** * Composable that renders the current dialog, if any. * This should be called once at the root level of your auth flow. - * - * Uses the existing [ErrorRecoveryDialog] component. + * + * Uses the existing [ErrorRecoveryDialog] component. Strings come from + * [LocalAuthUIStringProvider], read here at render time, unless the controller was built + * through the deprecated constructor that takes one explicitly. */ @Composable fun CurrentDialog() { + val stringProvider = explicitStringProvider ?: LocalAuthUIStringProvider.current val state = dialogState when (state) { is DialogState.ErrorDialog -> { @@ -174,16 +199,40 @@ class TopLevelDialogController( * live auth state on every [TopLevelDialogController.showErrorDialog] call without being * recreated (and losing its de-duplication history) whenever the auth state changes. * - * Keyed on [stringProvider] rather than left unkeyed: callers must pass a `remember`ed - * [stringProvider] (stable across recompositions), otherwise the controller — and its - * de-duplication history — would be recreated on every recomposition. + * The `remember` is deliberately unkeyed, so any key would be a way to lose a dialog that was + * just shown. Nothing kept across recompositions goes stale as a result: strings are resolved + * from [LocalAuthUIStringProvider] at render time, and [authState] is read through + * [rememberUpdatedState] rather than captured, so the first composition's lambda is not pinned + * for the controller's life. + */ +@Composable +fun rememberTopLevelDialogController( + authState: () -> AuthState +): TopLevelDialogController { + val currentAuthState by rememberUpdatedState(authState) + return remember { + TopLevelDialogController { currentAuthState() } + } +} + +/** + * Creates and remembers a [TopLevelDialogController] bound to an explicit [stringProvider]. + * + * Kept only for source compatibility. It still keys the `remember` on [stringProvider], so a + * caller whose provider is not stable across recompositions loses the controller's state — that + * is the reason to move to the single-argument overload above. */ +@Deprecated( + "The string provider is now read from LocalAuthUIStringProvider at render time.", + ReplaceWith("rememberTopLevelDialogController(authState)") +) @Composable fun rememberTopLevelDialogController( stringProvider: AuthUIStringProvider, authState: () -> AuthState ): TopLevelDialogController { return remember(stringProvider) { + @Suppress("DEPRECATION") TopLevelDialogController(stringProvider, authState) } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt index c986fcd4c5..94bd9efbc1 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt @@ -78,7 +78,6 @@ import com.firebase.ui.auth.configuration.auth_provider.rememberOAuthSignInHandl import com.firebase.ui.auth.configuration.auth_provider.rememberSignInWithFacebookLauncher import com.firebase.ui.auth.configuration.auth_provider.signInWithEmailLink import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider -import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider import com.firebase.ui.auth.configuration.theme.LocalAuthUITheme import com.firebase.ui.auth.ui.components.LocalTopLevelDialogController @@ -181,7 +180,9 @@ fun FirebaseAuthScreen( val activity = LocalActivity.current val context = LocalContext.current val coroutineScope = rememberCoroutineScope() - val stringProvider = remember(context) { DefaultAuthUIStringProvider(context) } + // The host's provider, not one built from LocalContext: only this honours a custom + // AuthUIStringProvider and the configured locale. + val stringProvider = configuration.stringProvider // The reauth effects below run outside composition, so they cannot call stringResource // themselves. @@ -202,7 +203,7 @@ fun FirebaseAuthScreen( hostAuthFlowScope(authUI, configuration, hostStateHolder) } val authState = rawAuthState - val dialogController = rememberTopLevelDialogController(stringProvider) { authState } + val dialogController = rememberTopLevelDialogController { authState } val lastSuccessfulUserId = remember { mutableStateOf(null) } val pendingLinkingCredential = remember { mutableStateOf(null) } val pendingResolver = remember { mutableStateOf(null) } @@ -412,6 +413,7 @@ fun FirebaseAuthScreen( metadata = authRouteMetadata(AuthRoute.MethodPicker) ) { if (customMethodPickerLayout != null) { + // Takes over the entire screen; see the KDoc on customMethodPickerLayout. Box(modifier = Modifier.fillMaxSize()) { customMethodPickerLayout(configuration.providers, onProviderSelected) } diff --git a/auth/src/main/java/com/firebase/ui/auth/util/ContinueUrlBuilder.kt b/auth/src/main/java/com/firebase/ui/auth/util/ContinueUrlBuilder.kt index 80efbd8bd4..427cef241a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/ContinueUrlBuilder.kt +++ b/auth/src/main/java/com/firebase/ui/auth/util/ContinueUrlBuilder.kt @@ -13,7 +13,9 @@ */ package com.firebase.ui.auth.util +import android.net.Uri import androidx.annotation.RestrictTo +import androidx.core.net.toUri import com.firebase.ui.auth.util.EmailLinkParser.LinkParameters.ANONYMOUS_USER_ID_IDENTIFIER import com.firebase.ui.auth.util.EmailLinkParser.LinkParameters.FORCE_SAME_DEVICE_IDENTIFIER import com.firebase.ui.auth.util.EmailLinkParser.LinkParameters.PROVIDER_ID_IDENTIFIER @@ -22,15 +24,20 @@ import com.firebase.ui.auth.util.EmailLinkParser.LinkParameters.SESSION_IDENTIFI /** * Builder for constructing continue URLs with embedded session and authentication parameters. * Used in email link sign-in flows to pass state between devices. + * + * The incoming URL comes from the consumer's [com.google.firebase.auth.ActionCodeSettings], so it + * may already carry a query string and/or a fragment. Parameters are appended through [Uri], the + * same parser [EmailLinkParser] reads them back with, which places them in the query whatever the + * URL's shape and percent-encodes their values. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class ContinueUrlBuilder(url: String) { - private val continueUrl: StringBuilder + private var continueUrl: Uri init { require(url.isNotBlank()) { "URL cannot be empty" } - continueUrl = StringBuilder(url).append("?") + continueUrl = url.toUri() } fun appendSessionId(sessionId: String): ContinueUrlBuilder { @@ -57,16 +64,9 @@ class ContinueUrlBuilder(url: String) { private fun addQueryParam(key: String, value: String) { if (value.isBlank()) return - val isFirstParam = continueUrl.last() == '?' - val mark = if (isFirstParam) "" else "&" - continueUrl.append("$mark$key=$value") + continueUrl = continueUrl.buildUpon().appendQueryParameter(key, value).build() } - fun build(): String { - if (continueUrl.last() == '?') { - // No params added so we remove the '?' - continueUrl.setLength(continueUrl.length - 1) - } - return continueUrl.toString() - } -} \ No newline at end of file + // Untouched when nothing was appended: Uri hands back the string it was parsed from. + fun build(): String = continueUrl.toString() +} diff --git a/auth/src/main/res/values-ar/strings.xml b/auth/src/main/res/values-ar/strings.xml index 8367da3751..64c66558ba 100755 --- a/auth/src/main/res/values-ar/strings.xml +++ b/auth/src/main/res/values-ar/strings.xml @@ -100,7 +100,7 @@ تمّ التحقّق تلقائيًا من رقم الهاتف. إعادة إرسال الرمز تأكيد ملكية رقم الهاتف - Use a different phone number + استخدام رقم هاتف آخر عند النقر على “%1$s”، قد يتمّ إرسال رسالة قصيرة SMS وقد يتمّ تطبيق رسوم الرسائل والبيانات. يشير النقر على "%1$s" إلى موافقتك على %2$s و%3$s. وقد يتمّ إرسال رسالة قصيرة كما قد تنطبق رسوم الرسائل والبيانات. خطأ في المصادقة @@ -165,7 +165,7 @@ إعادة إرسال بريد التحقق المفتاح السري تسجيل الخروج - مسجل الدخول باسم + مسجل الدخول باسم %1$s تخطي استخدم طريقة أخرى رمز التحقق @@ -175,4 +175,17 @@ المصادقة متعددة العوامل معطلة حاليًا + البريد الإلكتروني أو كلمة المرور غير صحيحة + لم تعد جلسة التحقق هذه صالحة. اطلب رمزًا جديدًا. + لم تكتمل عملية التحقق من رقم الهاتف. أعد المحاولة. + بيانات الاعتماد هذه تخص حسابًا آخر. + رقم الهاتف هذا غير مُعدّ للتحقق في هذا الحساب. + انتهت صلاحية جلسة تسجيل الدخول. سجِّل الدخول مرة أخرى للمتابعة. + لم يعد هذا الرابط صالحًا. اطلب رابطًا جديدًا. + تحقَّق من عنوان بريدك الإلكتروني قبل المتابعة. + طريقة التحقق هذه مُعدّة بالفعل في هذا الحساب. + لقد وصلت إلى الحد الأقصى لطرق التحقق في هذا الحساب. + كلمة المرور لا تستوفي المتطلبات. جرِّب كلمة مرور أخرى. + كلمة المرور طويلة جدًا. الحد الأقصى للطول هو %1$d. + تعذّر العثور على مفتاح مرور لهذا الحساب. سجِّل الدخول بطريقة أخرى. diff --git a/auth/src/main/res/values-b+es+419/strings.xml b/auth/src/main/res/values-b+es+419/strings.xml index 6039dc47f8..fef179567e 100755 --- a/auth/src/main/res/values-b+es+419/strings.xml +++ b/auth/src/main/res/values-b+es+419/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -110,17 +110,17 @@ La autenticación fue cancelada. Vuelva a intentarlo cuando esté listo. - Choose Authentication Method - Set Up SMS Verification - Set Up Authenticator App - Verify Your Code + Elige el método de autenticación + Configurar verificación por SMS + Configurar aplicación de autenticación + Verifica tu código - Select a second authentication method to secure your account - Enter your phone number to receive verification codes - Scan the QR code with your authenticator app - Enter the code sent to your phone - Enter the code from your authenticator app - Enter your verification code + Selecciona un segundo método de autenticación para proteger tu cuenta + Introduce tu número de teléfono para recibir códigos de verificación + Escanea el código QR con tu aplicación de autenticación + Introduce el código enviado a tu teléfono + Introduce el código de tu aplicación de autenticación + Introduce tu código de verificación Confirmar contraseña Las contraseñas no coinciden @@ -192,5 +192,18 @@ Reautenticar - Multi-factor authentication is currently disabled + La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-bg/strings.xml b/auth/src/main/res/values-bg/strings.xml index 479a681434..8b0744741e 100755 --- a/auth/src/main/res/values-bg/strings.xml +++ b/auth/src/main/res/values-bg/strings.xml @@ -100,7 +100,7 @@ Телефонният номер е потвърден автоматично Повторно изпращане на кода Потвърждаване на телефонния номер - Use a different phone number + Използване на друг телефонен номер Докосвайки „%1$s“, може да получите SMS съобщение. То може да се таксува по тарифите за данни и SMS. Докосвайки „%1$s", приемате нашите %2$s и %3$s. Възможно е да получите SMS съобщение. То може да се таксува по тарифите за данни и SMS. Грешка при удостоверяване @@ -165,7 +165,7 @@ Изпращане на имейл за потвърждение отново Таен ключ Изход - Влезли сте като + Влезли сте като %1$s Пропускане Използване на друг метод Код за потвърждение @@ -175,4 +175,17 @@ Многофакторната автентификация в момента е деактивирана + Имейл адресът или паролата не са правилни + Тази сесия за потвърждаване вече не е валидна. Заявете нов код. + Потвърждаването по телефон не завърши. Опитайте отново. + Тези идентификационни данни принадлежат на друг профил. + Този телефонен номер не е настроен за потвърждаване в този профил. + Сесията ви за вход изтече. Влезте отново, за да продължите. + Тази връзка вече не е валидна. Заявете нова. + Потвърдете имейл адреса си, преди да продължите. + Този метод за потвърждаване вече е настроен в този профил. + Достигнахте ограничението за методи за потвърждаване в този профил. + Паролата ви не отговаря на изискванията. Опитайте с друга. + Паролата е твърде дълга. Максималната дължина е %1$d. + Не намерихме код за достъп за този профил. Влезте по друг начин. diff --git a/auth/src/main/res/values-bn/strings.xml b/auth/src/main/res/values-bn/strings.xml index 6b0870da65..fdb7a9919c 100755 --- a/auth/src/main/res/values-bn/strings.xml +++ b/auth/src/main/res/values-bn/strings.xml @@ -100,15 +100,15 @@ ফোন নম্বরটি নিজে থেকে যাচাই করা হয়েছে কোডটি আবার পাঠান ফোন নম্বর যাচাই করুন - Use a different phone number + অন্য একটি ফোন নম্বর ব্যবহার করুন %1$s এ ট্যাপ করলে আপনি একটি এসএমএস পাঠাতে পারেন। মেসেজ ও ডেটার চার্জ প্রযোজ্য। “%1$s” বোতামে ট্যাপ করার অর্থ, আপনি আমাদের %2$s এবং %3$s-এর সাথে সম্মত। একটি এসএমএস পাঠানো হতে পারে। মেসেজ এবং ডেটার উপরে প্রযোজ্য চার্জ লাগতে পারে। - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + প্রমাণীকরণ সংক্রান্ত সমস্যা + আবার চেষ্টা করুন + অতিরিক্ত যাচাইকরণ প্রয়োজন। অনুগ্রহ করে মাল্টি-ফ্যাক্টর প্রমাণীকরণ সম্পূর্ণ করুন। + অ্যাকাউন্ট লিঙ্ক করা প্রয়োজন। অনুগ্রহ করে অন্য কোনও সাইন-ইন পদ্ধতি ব্যবহার করে দেখুন। + প্রমাণীকরণ বাতিল করা হয়েছে। আপনি প্রস্তুত হলে আবার চেষ্টা করুন। প্রমাণীকরণ পদ্ধতি চয়ন করুন @@ -166,7 +166,7 @@ যাচাইকরণ ইমেল পুনরায় পাঠান গোপন কী সাইন আউট - হিসাবে সাইন ইন করা হয়েছে + %1$s হিসাবে সাইন ইন করা হয়েছে এড়িয়ে যান একটি ভিন্ন পদ্ধতি ব্যবহার করুন যাচাইকরণ কোড @@ -176,4 +176,17 @@ মাল্টি-ফ্যাক্টর প্রমাণীকরণ বর্তমানে নিষ্ক্রিয় + ইমেল বা পাসওয়ার্ড সঠিক নয় + এই যাচাইকরণ সেশনটি আর বৈধ নেই। নতুন কোডের জন্য অনুরোধ করুন। + ফোন যাচাইকরণ সম্পূর্ণ হয়নি। আবার চেষ্টা করুন। + এই ক্রেডেনশিয়াল অন্য একটি অ্যাকাউন্টের। + এই অ্যাকাউন্টে যাচাইকরণের জন্য এই ফোন নম্বরটি সেট আপ করা নেই। + আপনার সাইন-ইন সেশনের মেয়াদ শেষ হয়ে গেছে। চালিয়ে যেতে আবার সাইন-ইন করুন। + এই লিঙ্কটি আর বৈধ নয়। নতুন একটির জন্য অনুরোধ করুন। + চালিয়ে যাওয়ার আগে আপনার ইমেল অ্যাড্রেস যাচাই করুন। + এই যাচাইকরণ পদ্ধতিটি এই অ্যাকাউন্টে ইতিমধ্যেই সেট আপ করা আছে। + এই অ্যাকাউন্টে যাচাইকরণ পদ্ধতির সীমায় আপনি পৌঁছে গেছেন। + আপনার পাসওয়ার্ড প্রয়োজনীয় শর্ত পূরণ করে না। অন্য একটি পাসওয়ার্ড ব্যবহার করুন। + পাসওয়ার্ড খুব বড়। সর্বাধিক দৈর্ঘ্য হল %1$d। + এই অ্যাকাউন্টের জন্য পাসকী খুঁজে পাওয়া যায়নি। অন্য উপায়ে সাইন-ইন করুন। diff --git a/auth/src/main/res/values-ca/strings.xml b/auth/src/main/res/values-ca/strings.xml index e457554c9d..8ac11d2c50 100755 --- a/auth/src/main/res/values-ca/strings.xml +++ b/auth/src/main/res/values-ca/strings.xml @@ -47,7 +47,7 @@ Nom i cognoms Desa T\'estàs registrant… - La contrasenya no és prou segura. Utilitza com a mínim %1$d caràcter i combina lletres i números. La contrasenya no és prou segura. Utilitza com a mínim %1$d caràcters i combina lletres i números. + La contrasenya no és prou segura. Utilitza com a mínim %1$d caràcter i combina lletres i números. La contrasenya no és prou segura. Utilitza com a mínim %1$d de caràcters i combina lletres i números. La contrasenya no és prou segura. Utilitza com a mínim %1$d caràcters i combina lletres i números. No s\'ha pogut registrar el compte de correu electrònic Condicions del servei política de privadesa @@ -100,15 +100,15 @@ El número de telèfon s\'ha verificat automàticament Torna a enviar el codi Verifica el número de telèfon - Use a different phone number + Utilitza un altre número de telèfon En tocar %1$s, és possible que s\'enviï un SMS. Es poden aplicar tarifes de dades i missatges. En tocar %1$s, acceptes les nostres %2$s i la nostra %3$s. És possible que s\'enviï un SMS. Es poden aplicar tarifes de dades i missatges. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Error d\'autenticació + Torna-ho a provar + Cal una verificació addicional. Completeu l\'autenticació multifactor. + Cal enllaçar el compte. Proveu un altre mètode d\'inici de sessió. + L\'autenticació s\'ha cancel·lat. Torneu-ho a provar quan estigueu a punt. Trieu el mètode d\'autenticació @@ -166,7 +166,7 @@ Torna a enviar el correu de verificació Clau secreta Tanca la sessió - Has iniciat sessió com a + Has iniciat sessió com a %1$s Omet Utilitza un mètode diferent Codi de verificació @@ -176,4 +176,17 @@ L\'autenticació multifactor està desactivada actualment + Aquest correu electrònic o aquesta contrasenya no són correctes + Aquesta sessió de verificació ja no és vàlida. Sol·licita un codi nou. + La verificació per telèfon no s\'ha completat. Torna-ho a provar. + Aquestes credencials pertanyen a un altre compte. + Aquest número de telèfon no està configurat per a la verificació en aquest compte. + La teva sessió ha caducat. Torna a iniciar la sessió per continuar. + Aquest enllaç ja no és vàlid. Sol·licita\'n un de nou. + Verifica la teva adreça electrònica abans de continuar. + Aquest mètode de verificació ja està configurat en aquest compte. + Has assolit el límit de mètodes de verificació d\'aquest compte. + La teva contrasenya no compleix els requisits. Prova\'n una altra. + La contrasenya és massa llarga. La longitud màxima és %1$d. + No hem trobat cap clau d\'accés per a aquest compte. Inicia la sessió d\'una altra manera. diff --git a/auth/src/main/res/values-cs/strings.xml b/auth/src/main/res/values-cs/strings.xml index 64e5b18c18..c4c1adce98 100755 --- a/auth/src/main/res/values-cs/strings.xml +++ b/auth/src/main/res/values-cs/strings.xml @@ -100,7 +100,7 @@ Telefonní číslo bylo automaticky ověřeno Znovu poslat kód Ověřit telefonní číslo - Use a different phone number + Použít jiné telefonní číslo Po klepnutí na možnost %1$s může být odeslána SMS. Mohou být účtovány poplatky za zprávy a data. Klepnutím na tlačítko %1$s vyjadřujete svůj souhlas s dokumenty %2$s a %3$s. Může být odeslána SMS a mohou být účtovány poplatky za zprávy a data. Chyba ověření @@ -165,7 +165,7 @@ Znovu odeslat ověřovací e-mail Tajný klíč Odhlásit se - Přihlášen jako + Přihlášen jako %1$s Přeskočit Použít jinou metodu Ověřovací kód @@ -175,4 +175,17 @@ Vícefaktorové ověřování je aktuálně zakázáno + E-mail nebo heslo nejsou správné + Tato ověřovací relace už není platná. Vyžádejte si nový kód. + Ověření telefonu se nedokončilo. Zkuste to znovu. + Tyto přihlašovací údaje patří k jinému účtu. + Toto telefonní číslo není u tohoto účtu nastaveno pro ověřování. + Platnost vašeho přihlášení vypršela. Pokračujte opětovným přihlášením. + Tento odkaz už není platný. Vyžádejte si nový. + Před pokračováním ověřte svou e-mailovou adresu. + Tento způsob ověření je u tohoto účtu už nastavený. + Dosáhli jste limitu způsobů ověření pro tento účet. + Vaše heslo nesplňuje požadavky. Zkuste jiné. + Heslo je příliš dlouhé. Maximální délka je %1$d. + Pro tento účet jsme nenašli žádný přístupový klíč. Přihlaste se jiným způsobem. diff --git a/auth/src/main/res/values-da/strings.xml b/auth/src/main/res/values-da/strings.xml index 52c217d8a9..7ae951c406 100755 --- a/auth/src/main/res/values-da/strings.xml +++ b/auth/src/main/res/values-da/strings.xml @@ -100,7 +100,7 @@ Telefonnummeret blev bekræftet automatisk Send koden igen Bekræft telefonnummer - Use a different phone number + Brug et andet telefonnummer Når du trykker på “%1$s”, sendes der måske en sms. Der opkræves muligvis gebyrer for beskeder og data. Når du trykker på "%1$s", indikerer du, at du accepterer vores %2$s og %3$s. Der sendes måske en sms. Der opkræves muligvis gebyrer for beskeder og data. Godkendelsesfejl @@ -165,7 +165,7 @@ Send bekræftelsesemail igen Hemmelig nøgle Log ud - Logget ind som + Logget ind som %1$s Spring over Brug en anden metode Bekræftelseskode @@ -175,4 +175,17 @@ Multifaktorgodkendelse er i øjeblikket deaktiveret + Mailadressen eller adgangskoden er ikke korrekt + Denne bekræftelsessession er ikke længere gyldig. Anmod om en ny kode. + Telefonbekræftelsen blev ikke fuldført. Prøv igen. + Disse loginoplysninger tilhører en anden konto. + Dette telefonnummer er ikke konfigureret til bekræftelse på denne konto. + Din loginsession er udløbet. Log ind igen for at fortsætte. + Dette link er ikke længere gyldigt. Anmod om et nyt. + Bekræft din mailadresse, før du fortsætter. + Denne bekræftelsesmetode er allerede konfigureret på denne konto. + Du har nået grænsen for bekræftelsesmetoder på denne konto. + Din adgangskode opfylder ikke kravene. Prøv en anden. + Adgangskoden er for lang. Den maksimale længde er %1$d. + Vi kunne ikke finde en adgangsnøgle til denne konto. Log ind på en anden måde. diff --git a/auth/src/main/res/values-de-rAT/strings.xml b/auth/src/main/res/values-de-rAT/strings.xml index ce10dd5d0d..1df316df21 100755 --- a/auth/src/main/res/values-de-rAT/strings.xml +++ b/auth/src/main/res/values-de-rAT/strings.xml @@ -100,14 +100,14 @@ Telefonnummer wurde automatisch bestätigt Code erneut senden Telefonnummer bestätigen - Use a different phone number + Eine andere Telefonnummer verwenden Wenn Sie auf “%1$s” tippen, erhalten Sie möglicherweise eine SMS. Es können Gebühren für SMS und Datenübertragung anfallen. Indem Sie auf “%1$s” tippen, stimmen Sie unseren %2$s und unserer %3$s zu. Sie erhalten möglicherweise eine SMS und es können Gebühren für die Nachricht und die Datenübertragung anfallen. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Authentifizierungsfehler + Erneut versuchen + Zusätzliche Verifizierung erforderlich. Bitte schließen Sie die Multi-Faktor-Authentifizierung ab. + Das Konto muss verknüpft werden. Bitte versuchen Sie eine andere Anmeldemethode. + Die Authentifizierung wurde abgebrochen. Bitte versuchen Sie es erneut, wenn Sie bereit sind. Authentifizierungsmethode auswählen @@ -193,4 +193,17 @@ Die Multi-Faktor-Authentifizierung ist derzeit deaktiviert + E-Mail-Adresse oder Passwort ist nicht korrekt + Diese Bestätigungssitzung ist nicht mehr gültig. Fordern Sie einen neuen Code an. + Die Telefonbestätigung wurde nicht abgeschlossen. Versuchen Sie es erneut. + Diese Anmeldedaten gehören zu einem anderen Konto. + Diese Telefonnummer ist für dieses Konto nicht zur Bestätigung eingerichtet. + Ihre Anmeldesitzung ist abgelaufen. Melden Sie sich erneut an, um fortzufahren. + Dieser Link ist nicht mehr gültig. Fordern Sie einen neuen an. + Bestätigen Sie Ihre E-Mail-Adresse, bevor Sie fortfahren. + Diese Bestätigungsmethode ist für dieses Konto bereits eingerichtet. + Sie haben die maximale Anzahl an Bestätigungsmethoden für dieses Konto erreicht. + Ihr Passwort erfüllt die Anforderungen nicht. Versuchen Sie es mit einem anderen. + Das Passwort ist zu lang. Die maximale Länge beträgt %1$d. + Für dieses Konto wurde kein Passkey gefunden. Melden Sie sich auf andere Weise an. diff --git a/auth/src/main/res/values-de-rCH/strings.xml b/auth/src/main/res/values-de-rCH/strings.xml index f2b070a17c..3b84b28184 100755 --- a/auth/src/main/res/values-de-rCH/strings.xml +++ b/auth/src/main/res/values-de-rCH/strings.xml @@ -100,15 +100,15 @@ Telefonnummer wurde automatisch bestätigt Code erneut senden Telefonnummer bestätigen - Use a different phone number + Eine andere Telefonnummer verwenden Wenn Sie auf “%1$s” tippen, erhalten Sie möglicherweise eine SMS. Es können Gebühren für SMS und Datenübertragung anfallen. Indem Sie auf “%1$s” tippen, stimmen Sie unseren %2$s und unserer %3$s zu. Sie erhalten möglicherweise eine SMS und es können Gebühren für die Nachricht und die Datenübertragung anfallen. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Authentifizierungsfehler + Erneut versuchen + Zusätzliche Verifizierung erforderlich. Bitte schließen Sie die Multi-Faktor-Authentifizierung ab. + Das Konto muss verknüpft werden. Bitte versuchen Sie eine andere Anmeldemethode. + Die Authentifizierung wurde abgebrochen. Bitte versuchen Sie es erneut, wenn Sie bereit sind. Authentifizierungsmethode auswählen @@ -194,4 +194,17 @@ Die Multi-Faktor-Authentifizierung ist derzeit deaktiviert + E-Mail-Adresse oder Passwort ist nicht korrekt + Diese Bestätigungssitzung ist nicht mehr gültig. Fordern Sie einen neuen Code an. + Die Telefonbestätigung wurde nicht abgeschlossen. Versuchen Sie es erneut. + Diese Anmeldedaten gehören zu einem anderen Konto. + Diese Telefonnummer ist für dieses Konto nicht zur Bestätigung eingerichtet. + Ihre Anmeldesitzung ist abgelaufen. Melden Sie sich erneut an, um fortzufahren. + Dieser Link ist nicht mehr gültig. Fordern Sie einen neuen an. + Bestätigen Sie Ihre E-Mail-Adresse, bevor Sie fortfahren. + Diese Bestätigungsmethode ist für dieses Konto bereits eingerichtet. + Sie haben die maximale Anzahl an Bestätigungsmethoden für dieses Konto erreicht. + Ihr Passwort erfüllt die Anforderungen nicht. Versuchen Sie es mit einem anderen. + Das Passwort ist zu lang. Die maximale Länge beträgt %1$d. + Für dieses Konto wurde kein Passkey gefunden. Melden Sie sich auf andere Weise an. diff --git a/auth/src/main/res/values-de/strings.xml b/auth/src/main/res/values-de/strings.xml index 9fe8f66f83..74b54f72f3 100755 --- a/auth/src/main/res/values-de/strings.xml +++ b/auth/src/main/res/values-de/strings.xml @@ -100,7 +100,7 @@ Telefonnummer wurde automatisch bestätigt Code erneut senden Telefonnummer bestätigen - Use a different phone number + Eine andere Telefonnummer verwenden Wenn Sie auf “%1$s” tippen, erhalten Sie möglicherweise eine SMS. Es können Gebühren für SMS und Datenübertragung anfallen. Indem Sie auf "%1$s" tippen, stimmen Sie unseren %2$s und unserer %3$s zu. Sie erhalten möglicherweise eine SMS und es können Gebühren für die Nachricht und die Datenübertragung anfallen. Authentifizierungsfehler @@ -193,4 +193,17 @@ Die Multi-Faktor-Authentifizierung ist derzeit deaktiviert + E-Mail-Adresse oder Passwort ist nicht korrekt + Diese Bestätigungssitzung ist nicht mehr gültig. Fordern Sie einen neuen Code an. + Die Telefonbestätigung wurde nicht abgeschlossen. Versuchen Sie es erneut. + Diese Anmeldedaten gehören zu einem anderen Konto. + Diese Telefonnummer ist für dieses Konto nicht zur Bestätigung eingerichtet. + Ihre Anmeldesitzung ist abgelaufen. Melden Sie sich erneut an, um fortzufahren. + Dieser Link ist nicht mehr gültig. Fordern Sie einen neuen an. + Bestätigen Sie Ihre E-Mail-Adresse, bevor Sie fortfahren. + Diese Bestätigungsmethode ist für dieses Konto bereits eingerichtet. + Sie haben die maximale Anzahl an Bestätigungsmethoden für dieses Konto erreicht. + Ihr Passwort erfüllt die Anforderungen nicht. Versuchen Sie es mit einem anderen. + Das Passwort ist zu lang. Die maximale Länge beträgt %1$d. + Für dieses Konto wurde kein Passkey gefunden. Melden Sie sich auf andere Weise an. diff --git a/auth/src/main/res/values-el/strings.xml b/auth/src/main/res/values-el/strings.xml index 0854ec409f..c063f059a2 100755 --- a/auth/src/main/res/values-el/strings.xml +++ b/auth/src/main/res/values-el/strings.xml @@ -100,15 +100,15 @@ Ο αριθμός τηλεφώνου επαληθεύτηκε αυτόματα Επανάληψη αποστολής κωδικού Επαλήθευση αριθμού τηλεφώνου - Use a different phone number + Χρήση διαφορετικού αριθμού τηλεφώνου Αν πατήσετε “%1$s”, μπορεί να σταλεί ένα SMS. Ενδέχεται να ισχύουν χρεώσεις μηνυμάτων και δεδομένων. Αν πατήσετε “%1$s”, δηλώνετε ότι αποδέχεστε τους %2$s και την %3$s. Μπορεί να σταλεί ένα SMS. Ενδέχεται να ισχύουν χρεώσεις μηνυμάτων και δεδομένων. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Σφάλμα ελέγχου ταυτότητας + Δοκιμάστε ξανά + Απαιτείται πρόσθετη επαλήθευση. Ολοκληρώστε τον έλεγχο ταυτότητας πολλαπλών παραγόντων. + Ο λογαριασμός πρέπει να συνδεθεί. Δοκιμάστε διαφορετική μέθοδο σύνδεσης. + Ο έλεγχος ταυτότητας ακυρώθηκε. Δοκιμάστε ξανά όταν είστε έτοιμοι. Επιλέξτε μέθοδο ελέγχου ταυτότητας @@ -166,7 +166,7 @@ Επαναποστολή email επαλήθευσης Μυστικό κλειδί Αποσύνδεση - Συνδεδεμένος ως + Συνδεδεμένος ως %1$s Παράλειψη Χρήση διαφορετικής μεθόδου Κωδικός επαλήθευσης @@ -176,4 +176,17 @@ Ο έλεγχος ταυτότητας πολλαπλών παραγόντων είναι απενεργοποιημένος προς το παρόν + Το ηλεκτρονικό ταχυδρομείο ή ο κωδικός πρόσβασης δεν είναι σωστά + Αυτή η περίοδος επαλήθευσης δεν είναι πλέον έγκυρη. Ζητήστε νέο κωδικό. + Η επαλήθευση τηλεφώνου δεν ολοκληρώθηκε. Δοκιμάστε ξανά. + Αυτά τα διαπιστευτήρια ανήκουν σε διαφορετικό λογαριασμό. + Αυτός ο αριθμός τηλεφώνου δεν έχει ρυθμιστεί για επαλήθευση σε αυτόν τον λογαριασμό. + Η περίοδος σύνδεσής σας έληξε. Συνδεθείτε ξανά για να συνεχίσετε. + Αυτός ο σύνδεσμος δεν είναι πλέον έγκυρος. Ζητήστε νέον. + Επαληθεύστε τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας πριν συνεχίσετε. + Αυτή η μέθοδος επαλήθευσης έχει ήδη ρυθμιστεί σε αυτόν τον λογαριασμό. + Έχετε φτάσει το όριο μεθόδων επαλήθευσης για αυτόν τον λογαριασμό. + Ο κωδικός πρόσβασής σας δεν πληροί τις προϋποθέσεις. Δοκιμάστε έναν άλλον. + Ο κωδικός πρόσβασης είναι πολύ μεγάλος. Το μέγιστο μήκος είναι %1$d. + Δεν βρέθηκε κλειδί πρόσβασης για αυτόν τον λογαριασμό. Συνδεθείτε με άλλον τρόπο. diff --git a/auth/src/main/res/values-en-rAU/strings.xml b/auth/src/main/res/values-en-rAU/strings.xml index 7f8dab7e63..37c78d4f41 100755 --- a/auth/src/main/res/values-en-rAU/strings.xml +++ b/auth/src/main/res/values-en-rAU/strings.xml @@ -124,7 +124,7 @@ Confirm password Passwords do not match - Password must be at least %1$d characters long + Password must be at least %1$d characters long Password must contain at least one uppercase letter Password must contain at least one lowercase letter Password must contain at least one number @@ -165,7 +165,7 @@ Resend verification email Secret key Sign out - Signed in as + Signed in as %1$s Skip Use a different method Verification code @@ -175,4 +175,17 @@ Multi-factor authentication is currently disabled + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + Password is too long. The maximum length is %1$d. + We couldn\'t find a passkey for this account. Sign in another way. diff --git a/auth/src/main/res/values-en-rCA/strings.xml b/auth/src/main/res/values-en-rCA/strings.xml index 2476ca4b8b..fce512a3be 100755 --- a/auth/src/main/res/values-en-rCA/strings.xml +++ b/auth/src/main/res/values-en-rCA/strings.xml @@ -124,7 +124,7 @@ Confirm password Passwords do not match - Password must be at least %1$d characters long + Password must be at least %1$d characters long Password must contain at least one upper-case letter Password must contain at least one lower-case letter Password must contain at least one number @@ -165,7 +165,7 @@ Resend verification email Secret key Sign out - Signed in as + Signed in as %1$s Skip Use a different method Verification code @@ -175,4 +175,17 @@ Multi-factor authentication is currently disabled + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + Password is too long. The maximum length is %1$d. + We couldn\'t find a passkey for this account. Sign in another way. diff --git a/auth/src/main/res/values-en-rGB/strings.xml b/auth/src/main/res/values-en-rGB/strings.xml index 8c19093629..741c00bb0a 100755 --- a/auth/src/main/res/values-en-rGB/strings.xml +++ b/auth/src/main/res/values-en-rGB/strings.xml @@ -124,7 +124,7 @@ Confirm password Passwords do not match - Password must be at least %1$d characters long + Password must be at least %1$d characters long Password must contain at least one upper-case letter Password must contain at least one lower-case letter Password must contain at least one number @@ -165,7 +165,7 @@ Resend verification email Secret key Sign out - Signed in as + Signed in as %1$s Skip Use a different method Verification code @@ -175,4 +175,17 @@ Multi-factor authentication is currently disabled + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + Password is too long. The maximum length is %1$d. + We couldn\'t find a passkey for this account. Sign in another way. diff --git a/auth/src/main/res/values-en-rIE/strings.xml b/auth/src/main/res/values-en-rIE/strings.xml index ee4d48e585..935bdcb019 100755 --- a/auth/src/main/res/values-en-rIE/strings.xml +++ b/auth/src/main/res/values-en-rIE/strings.xml @@ -158,7 +158,7 @@ Resend verification email Secret key Sign out - Signed in as + Signed in as %1$s Skip Use a different method Verification code @@ -168,4 +168,17 @@ Multi-factor authentication is currently disabled + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + Password is too long. The maximum length is %1$d. + We couldn\'t find a passkey for this account. Sign in another way. diff --git a/auth/src/main/res/values-en-rIN/strings.xml b/auth/src/main/res/values-en-rIN/strings.xml index ee4d48e585..935bdcb019 100755 --- a/auth/src/main/res/values-en-rIN/strings.xml +++ b/auth/src/main/res/values-en-rIN/strings.xml @@ -158,7 +158,7 @@ Resend verification email Secret key Sign out - Signed in as + Signed in as %1$s Skip Use a different method Verification code @@ -168,4 +168,17 @@ Multi-factor authentication is currently disabled + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + Password is too long. The maximum length is %1$d. + We couldn\'t find a passkey for this account. Sign in another way. diff --git a/auth/src/main/res/values-en-rSG/strings.xml b/auth/src/main/res/values-en-rSG/strings.xml index ee4d48e585..935bdcb019 100755 --- a/auth/src/main/res/values-en-rSG/strings.xml +++ b/auth/src/main/res/values-en-rSG/strings.xml @@ -158,7 +158,7 @@ Resend verification email Secret key Sign out - Signed in as + Signed in as %1$s Skip Use a different method Verification code @@ -168,4 +168,17 @@ Multi-factor authentication is currently disabled + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + Password is too long. The maximum length is %1$d. + We couldn\'t find a passkey for this account. Sign in another way. diff --git a/auth/src/main/res/values-en-rZA/strings.xml b/auth/src/main/res/values-en-rZA/strings.xml index ee4d48e585..935bdcb019 100755 --- a/auth/src/main/res/values-en-rZA/strings.xml +++ b/auth/src/main/res/values-en-rZA/strings.xml @@ -158,7 +158,7 @@ Resend verification email Secret key Sign out - Signed in as + Signed in as %1$s Skip Use a different method Verification code @@ -168,4 +168,17 @@ Multi-factor authentication is currently disabled + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + Password is too long. The maximum length is %1$d. + We couldn\'t find a passkey for this account. Sign in another way. diff --git a/auth/src/main/res/values-es-rAR/strings.xml b/auth/src/main/res/values-es-rAR/strings.xml index 6e6ec21946..8044dd5d37 100755 --- a/auth/src/main/res/values-es-rAR/strings.xml +++ b/auth/src/main/res/values-es-rAR/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rBO/strings.xml b/auth/src/main/res/values-es-rBO/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rBO/strings.xml +++ b/auth/src/main/res/values-es-rBO/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rCL/strings.xml b/auth/src/main/res/values-es-rCL/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rCL/strings.xml +++ b/auth/src/main/res/values-es-rCL/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rCO/strings.xml b/auth/src/main/res/values-es-rCO/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rCO/strings.xml +++ b/auth/src/main/res/values-es-rCO/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rCR/strings.xml b/auth/src/main/res/values-es-rCR/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rCR/strings.xml +++ b/auth/src/main/res/values-es-rCR/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rDO/strings.xml b/auth/src/main/res/values-es-rDO/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rDO/strings.xml +++ b/auth/src/main/res/values-es-rDO/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rEC/strings.xml b/auth/src/main/res/values-es-rEC/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rEC/strings.xml +++ b/auth/src/main/res/values-es-rEC/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rGT/strings.xml b/auth/src/main/res/values-es-rGT/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rGT/strings.xml +++ b/auth/src/main/res/values-es-rGT/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rHN/strings.xml b/auth/src/main/res/values-es-rHN/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rHN/strings.xml +++ b/auth/src/main/res/values-es-rHN/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rMX/strings.xml b/auth/src/main/res/values-es-rMX/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rMX/strings.xml +++ b/auth/src/main/res/values-es-rMX/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rNI/strings.xml b/auth/src/main/res/values-es-rNI/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rNI/strings.xml +++ b/auth/src/main/res/values-es-rNI/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rPA/strings.xml b/auth/src/main/res/values-es-rPA/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rPA/strings.xml +++ b/auth/src/main/res/values-es-rPA/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rPE/strings.xml b/auth/src/main/res/values-es-rPE/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rPE/strings.xml +++ b/auth/src/main/res/values-es-rPE/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rPR/strings.xml b/auth/src/main/res/values-es-rPR/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rPR/strings.xml +++ b/auth/src/main/res/values-es-rPR/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rPY/strings.xml b/auth/src/main/res/values-es-rPY/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rPY/strings.xml +++ b/auth/src/main/res/values-es-rPY/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rSV/strings.xml b/auth/src/main/res/values-es-rSV/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rSV/strings.xml +++ b/auth/src/main/res/values-es-rSV/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rUS/strings.xml b/auth/src/main/res/values-es-rUS/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rUS/strings.xml +++ b/auth/src/main/res/values-es-rUS/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rUY/strings.xml b/auth/src/main/res/values-es-rUY/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rUY/strings.xml +++ b/auth/src/main/res/values-es-rUY/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es-rVE/strings.xml b/auth/src/main/res/values-es-rVE/strings.xml index f95dda99e2..3774b38b65 100755 --- a/auth/src/main/res/values-es-rVE/strings.xml +++ b/auth/src/main/res/values-es-rVE/strings.xml @@ -47,7 +47,7 @@ Nombre y apellido Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números + La contraseña no es lo suficientemente segura. Usa al menos %1$d carácter y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d de caracteres y una combinación de letras y números La contraseña no es lo suficientemente segura. Usa al menos %1$d caracteres y una combinación de letras y números No se pudo registrar la cuenta de correo electrónico Condiciones del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se verificó automáticamente el número de teléfono Reenviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos. Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos. Error de autenticación @@ -186,4 +186,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + No se completó la verificación telefónica. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión expiró. Vuelve a acceder para continuar. + Este vínculo ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Alcanzaste el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple con los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No encontramos una llave de acceso para esta cuenta. Accede de otra manera. diff --git a/auth/src/main/res/values-es/strings.xml b/auth/src/main/res/values-es/strings.xml index ac5767d33a..fd7e4de5ea 100755 --- a/auth/src/main/res/values-es/strings.xml +++ b/auth/src/main/res/values-es/strings.xml @@ -47,7 +47,7 @@ Nombre y apellidos Guardar Registrando… - La contraseña no es lo suficientemente segura. Usa %1$d carácter como mínimo y combina letras y números. La contraseña no es lo suficientemente segura. Usa %1$d caracteres como mínimo y combina letras y números. + La contraseña no es lo suficientemente segura. Usa %1$d carácter como mínimo y combina letras y números. La contraseña no es lo suficientemente segura. Usa %1$d de caracteres como mínimo y combina letras y números. La contraseña no es lo suficientemente segura. Usa %1$d caracteres como mínimo y combina letras y números. No se ha podido registrar la cuenta de correo electrónico Términos del Servicio Política de Privacidad @@ -100,7 +100,7 @@ Se ha verificado automáticamente el número de teléfono Volver a enviar código Verificar número de teléfono - Use a different phone number + Usar otro número de teléfono Al tocar %1$s, podría enviarse un SMS. Es posible que se apliquen cargos de mensajería y de uso de datos. Si tocas %1$s, confirmas que aceptas nuestras %2$s y nuestra %3$s. Podría enviarse un SMS, por lo que es posible que se apliquen cargos de mensajería y de uso de datos. Error de autenticación @@ -193,4 +193,17 @@ La autenticación multifactor está actualmente desactivada + El correo electrónico o la contraseña no son correctos + Esta sesión de verificación ya no es válida. Solicita un código nuevo. + La verificación telefónica no se ha completado. Vuelve a intentarlo. + Estas credenciales pertenecen a otra cuenta. + Este número de teléfono no está configurado para la verificación en esta cuenta. + Tu sesión ha caducado. Vuelve a iniciar sesión para continuar. + Este enlace ya no es válido. Solicita uno nuevo. + Verifica tu dirección de correo electrónico antes de continuar. + Este método de verificación ya está configurado en esta cuenta. + Has alcanzado el límite de métodos de verificación de esta cuenta. + Tu contraseña no cumple los requisitos. Prueba con otra. + La contraseña es demasiado larga. La longitud máxima es %1$d. + No hemos encontrado ninguna clave de acceso para esta cuenta. Inicia sesión de otra forma. diff --git a/auth/src/main/res/values-fa/strings.xml b/auth/src/main/res/values-fa/strings.xml index a7f6c74701..a9d911130c 100755 --- a/auth/src/main/res/values-fa/strings.xml +++ b/auth/src/main/res/values-fa/strings.xml @@ -100,15 +100,15 @@ شماره تلفن به‌طور خودکار به‌تأیید رسید ارسال مجدد کد تأیید شماره تلفن - Use a different phone number + استفاده از شماره تلفن دیگر با ضربه زدن روی «%1$s»، پیامکی برایتان ارسال می‌شود. هزینه پیام و داده اعمال می‌شود. درصورت ضربه‌زدن روی «%1$s»، موافقتتان را با %2$s و %3$s اعلام می‌کنید. پیامکی ارسال می‌شود. ممکن است هزینه داده و «پیام» محاسبه شود. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + خطای احراز هویت + دوباره امتحان کنید + تأیید بیشتری لازم است. لطفاً احراز هویت چندعاملی را کامل کنید. + حساب باید پیوند داده شود. لطفاً روش ورود دیگری را امتحان کنید. + احراز هویت لغو شد. هروقت آماده بودید، دوباره امتحان کنید. روش احراز هویت را انتخاب کنید @@ -166,7 +166,7 @@ ارسال مجدد ایمیل تأیید کلید مخفی خروج - وارد شده به عنوان + وارد شده به عنوان %1$s رد شدن استفاده از روش دیگر کد تأیید @@ -176,4 +176,17 @@ احراز هویت چند مرحله‌ای در حال حاضر غیرفعال است + آن ایمیل یا گذرواژه درست نیست + این جلسه تأیید دیگر معتبر نیست. کد جدیدی درخواست کنید. + تأیید شماره تلفن کامل نشد. دوباره امتحان کنید. + این اطلاعات ورود به حساب دیگری تعلق دارد. + این شماره تلفن برای تأیید در این حساب تنظیم نشده است. + جلسه ورود به سیستم شما منقضی شد. برای ادامه، دوباره وارد سیستم شوید. + این پیوند دیگر معتبر نیست. پیوند جدیدی درخواست کنید. + پیش از ادامه، نشانی ایمیلتان را تأیید کنید. + این روش تأیید قبلاً در این حساب تنظیم شده است. + به حداکثر تعداد روش‌های تأیید در این حساب رسیده‌اید. + گذرواژه شما الزامات را برآورده نمی‌کند. گذرواژه دیگری را امتحان کنید. + گذرواژه خیلی طولانی است. حداکثر طول %1$d است. + کلید عبوری برای این حساب پیدا نشد. به روش دیگری وارد سیستم شوید. diff --git a/auth/src/main/res/values-fi/strings.xml b/auth/src/main/res/values-fi/strings.xml index 1dbdcef0d0..391636afc6 100755 --- a/auth/src/main/res/values-fi/strings.xml +++ b/auth/src/main/res/values-fi/strings.xml @@ -100,7 +100,7 @@ Puhelinnumero vahvistettu automaattisesti Lähetä koodi uudelleen Vahvista puhelinnumero - Use a different phone number + Käytä toista puhelinnumeroa Kun napautat %1$s, tekstiviesti voidaan lähettää. Datan ja viestien käyttö voi olla maksullista. Napauttamalla %1$s vahvistat hyväksyväsi seuraavat: %2$s ja %3$s. Tekstiviesti voidaan lähettää, ja datan ja viestien käyttö voi olla maksullista. Todennusvirhe @@ -165,7 +165,7 @@ Lähetä vahvistussähköposti uudelleen Salainen avain Kirjaudu ulos - Kirjautuneena nimellä + Kirjautuneena nimellä %1$s Ohita Käytä eri menetelmää Vahvistuskoodi @@ -175,4 +175,17 @@ Monivaiheinen todennus on tällä hetkellä poistettu käytöstä + Sähköposti tai salasana on virheellinen + Tämä vahvistusistunto ei ole enää voimassa. Pyydä uusi koodi. + Puhelinvahvistus ei valmistunut. Yritä uudelleen. + Nämä tunnistetiedot kuuluvat toiselle tilille. + Tätä puhelinnumeroa ei ole määritetty vahvistukseen tällä tilillä. + Kirjautumisistuntosi vanheni. Kirjaudu sisään uudelleen jatkaaksesi. + Tämä linkki ei ole enää voimassa. Pyydä uusi. + Vahvista sähköpostiosoitteesi ennen kuin jatkat. + Tämä vahvistustapa on jo määritetty tällä tilillä. + Olet saavuttanut tämän tilin vahvistustapojen enimmäismäärän. + Salasanasi ei täytä vaatimuksia. Kokeile toista. + Salasana on liian pitkä. Enimmäispituus on %1$d. + Tälle tilille ei löytynyt avainkoodia. Kirjaudu sisään toisella tavalla. diff --git a/auth/src/main/res/values-fil/strings.xml b/auth/src/main/res/values-fil/strings.xml index 39b866524a..3dda0a5b3e 100755 --- a/auth/src/main/res/values-fil/strings.xml +++ b/auth/src/main/res/values-fil/strings.xml @@ -100,7 +100,7 @@ Awtomatikong na-verify ang numero ng telepono Ipadala Muli ang Code I-verify ang Numero ng Telepono - Use a different phone number + Gumamit ng ibang numero ng telepono Sa pag-tap sa “%1$s,“ maaaring magpadala ng SMS. Maaaring ipatupad ang mga rate ng pagmemensahe at data. Sa pag-tap sa “%1$s”, ipinababatid mo na tinatanggap mo ang aming %2$s at %3$s. Maaaring magpadala ng SMS. Maaaring ipatupad ang mga rate ng pagmemensahe at data. Error sa Pagpapatotoo @@ -146,7 +146,7 @@ Pumili ng paraan ng pag-verify Magdagdag ng karagdagang layer ng seguridad SMS - Authenticator app + App ng authenticator Ang numerong ito ay nauugnay sa ibang account Kinakailangan ang pag-verify I-scan ang QR code gamit ang iyong authenticator app @@ -163,16 +163,29 @@ Mag-authenticate muli Alisin Ipadala muli ang verification email - Secret key + Sikretong key Mag-sign out - Naka-sign in bilang + Naka-sign in bilang %1$s Laktawan Gumamit ng ibang paraan - Verification code + Code sa pag-verify Na-verify ang email I-verify Nagpadala kami ng verification email sa %1$s Kasalukuyang naka-disable ang multi-factor authentication + Mali ang email o password na iyon + Wala nang bisa ang verification session na iyon. Humiling ng bagong code. + Hindi nakumpleto ang pag-verify ng telepono. Subukang muli. + Kabilang ang mga kredensyal na iyon sa ibang account. + Hindi naka-set up ang numero ng teleponong iyon para sa pag-verify sa account na ito. + Nag-expire na ang iyong sign-in session. Mag-sign in muli para magpatuloy. + Wala nang bisa ang link na iyon. Humiling ng bago. + I-verify ang iyong email address bago ka magpatuloy. + Naka-set up na ang paraan ng pag-verify na iyon sa account na ito. + Naabot mo na ang limitasyon para sa mga paraan ng pag-verify sa account na ito. + Hindi natutugunan ng iyong password ang mga kinakailangan. Sumubok ng iba. + Masyadong mahaba ang password. Ang maximum na haba ay %1$d. + Wala kaming nakitang passkey para sa account na ito. Mag-sign in sa ibang paraan. diff --git a/auth/src/main/res/values-fr-rCH/strings.xml b/auth/src/main/res/values-fr-rCH/strings.xml index 447df07468..9f9928e4bc 100755 --- a/auth/src/main/res/values-fr-rCH/strings.xml +++ b/auth/src/main/res/values-fr-rCH/strings.xml @@ -47,7 +47,7 @@ Nom et prénom Enregistrer Inscription… - Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractère et une combinaison de chiffres et de lettres. Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractères et une combinaison de chiffres et de lettres. + Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractère et une combinaison de chiffres et de lettres. Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d de caractères et une combinaison de chiffres et de lettres. Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractères et une combinaison de chiffres et de lettres. Échec de la création du compte avec une adresse e-mail Conditions d\'utilisation Règles de confidentialité @@ -100,15 +100,15 @@ Numéro de téléphone validé automatiquement Renvoyer le code Valider le numéro de téléphone - Use a different phone number + Utiliser un autre numéro de téléphone En appuyant sur “%1$s”, vous déclencherez peut-être l\'envoi d\'un SMS. Des frais de messages et de données peuvent être facturés. En appuyant sur “%1$s”, vous acceptez les %2$s et les %3$s. Vous déclencherez peut-être l\'envoi d\'un SMS. Des frais de messages et de données peuvent être facturés. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Erreur d\'authentification + Réessayer + Vérification supplémentaire requise. Veuillez compléter l\'authentification à plusieurs facteurs. + Le compte doit être lié. Veuillez essayer une méthode de connexion différente. + L\'authentification a été annulée. Veuillez réessayer quand vous serez prêt. Choisir la méthode d\'authentification @@ -187,4 +187,17 @@ L\'authentification multifacteur est actuellement désactivée + Cet e-mail ou ce mot de passe est incorrect + Cette session de vérification n\'est plus valide. Demandez un nouveau code. + La vérification du numéro de téléphone n\'a pas abouti. Veuillez réessayer. + Ces identifiants appartiennent à un autre compte. + Ce numéro de téléphone n\'est pas configuré comme méthode de vérification sur ce compte. + Votre session de connexion a expiré. Reconnectez-vous pour continuer. + Ce lien n\'est plus valide. Demandez-en un nouveau. + Veuillez vérifier votre adresse e-mail avant de continuer. + Cette méthode de vérification est déjà configurée sur ce compte. + Vous avez atteint la limite de méthodes de vérification pour ce compte. + Votre mot de passe ne respecte pas les exigences. Essayez-en un autre. + Le mot de passe est trop long. La longueur maximale est de %1$d. + Aucune clé d\'accès n\'a été trouvée pour ce compte. Connectez-vous d\'une autre manière. diff --git a/auth/src/main/res/values-fr/strings.xml b/auth/src/main/res/values-fr/strings.xml index e6e2d773ca..c23fb48dd1 100755 --- a/auth/src/main/res/values-fr/strings.xml +++ b/auth/src/main/res/values-fr/strings.xml @@ -47,7 +47,7 @@ Nom et prénom Enregistrer Inscription… - Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractère et une combinaison de chiffres et de lettres. Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractères et une combinaison de chiffres et de lettres. + Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractère et une combinaison de chiffres et de lettres. Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d de caractères et une combinaison de chiffres et de lettres. Le mot de passe n\'est pas assez sécurisé. Utilisez au moins %1$d caractères et une combinaison de chiffres et de lettres. Échec de la création du compte avec une adresse e-mail Conditions d\'utilisation Règles de confidentialité @@ -100,7 +100,7 @@ Numéro de téléphone validé automatiquement Renvoyer le code Valider le numéro de téléphone - Use a different phone number + Utiliser un autre numéro de téléphone En appuyant sur “%1$s”, vous déclencherez peut-être l\'envoi d\'un SMS. Des frais de messages et de données peuvent être facturés. En appuyant sur "%1$s", vous acceptez les %2$s et les %3$s. Vous déclencherez peut-être l\'envoi d\'un SMS. Des frais de messages et de données peuvent être facturés. Erreur d\'authentification @@ -193,4 +193,17 @@ L\'authentification multifacteur est actuellement désactivée + L\'adresse e-mail ou le mot de passe est incorrect + Cette session de vérification n\'est plus valide. Demandez un nouveau code. + La vérification du numéro de téléphone n\'a pas abouti. Réessayez. + Ces identifiants appartiennent à un autre compte. + Ce numéro de téléphone n\'est pas configuré pour la vérification sur ce compte. + Votre session de connexion a expiré. Reconnectez-vous pour continuer. + Ce lien n\'est plus valide. Demandez-en un nouveau. + Vérifiez votre adresse e-mail avant de continuer. + Cette méthode de vérification est déjà configurée sur ce compte. + Vous avez atteint la limite de méthodes de vérification pour ce compte. + Votre mot de passe ne respecte pas les exigences. Essayez-en un autre. + Le mot de passe est trop long. La longueur maximale est de %1$d. + Aucune clé d\'accès n\'a été trouvée pour ce compte. Connectez-vous d\'une autre manière. diff --git a/auth/src/main/res/values-gsw/strings.xml b/auth/src/main/res/values-gsw/strings.xml index ca2068e978..6f45c47882 100755 --- a/auth/src/main/res/values-gsw/strings.xml +++ b/auth/src/main/res/values-gsw/strings.xml @@ -100,7 +100,7 @@ Telefonnummer wurde automatisch bestätigt Code erneut senden Telefonnummer bestätigen - Use a different phone number + E anderi Telefonnummere verwände Wenn Sie auf “%1$s” tippen, erhalten Sie möglicherweise eine SMS. Es können Gebühren für SMS und Datenübertragung anfallen. Indem Sie auf “%1$s” tippen, stimmen Sie unseren %2$s und unserer %3$s zu. Sie erhalten möglicherweise eine SMS und es können Gebühren für die Nachricht und die Datenübertragung anfallen. Authentifizierungsfehler @@ -165,7 +165,7 @@ Verifizierigs-E-Mail erneut sende Gheime Schlüssel Abmelde - Agmeldet als + Agmeldet als %1$s Überspringe E anderi Methode verwände Verifizierigscode @@ -175,4 +175,17 @@ D\'Multi-Faktor-Authentifizierig isch zurziit deaktiviert + Die E-Mail-Adrässe oder s Passwort isch nöd richtig + Die Bestätigungssitzig isch nüme gültig. Fordere Sie e neue Code aa. + D\'Telefonbestätigung isch nöd abgschlosse worde. Versuche Sie es erneut. + Die Aamäldedate ghöre zunemene andere Konto. + Die Telefonnummere isch uf däm Konto nöd als Bestätigungsmethode iigrichtet. + Ihri Aamäldig isch abglaufe. Mälde Sie sich erneut aa, zum wiiterzmache. + De Link isch nüme gültig. Fordere Sie e neue aa. + Bestätige Sie Ihri E-Mail-Adrässe, bevor Sie wiitermache. + Die Bestätigungsmethode isch scho uf däm Konto iigrichtet. + Sie händ d\'Grenze für Bestätigungsmethode uf däm Konto erreicht. + Ihres Passwort erfüllt d Aaforderige nöd. Probiere Sie es mit eme andere. + S Passwort isch z lang. D maximali Läng isch %1$d. + Für das Konto isch kein Passkey gfunde worde. Mälde Sie sich uf en anderi Art aa. diff --git a/auth/src/main/res/values-gu/strings.xml b/auth/src/main/res/values-gu/strings.xml index 38258f327a..a0736df0be 100755 --- a/auth/src/main/res/values-gu/strings.xml +++ b/auth/src/main/res/values-gu/strings.xml @@ -100,15 +100,15 @@ ફોન નંબર આપમેળે ચકાસવામાં આવ્યો કોડ ફરીથી મોકલો ફોન નંબર ચકાસો - Use a different phone number + અલગ ફોન નંબરનો ઉપયોગ કરો “%1$s”ને ટૅપ કરવાથી, કદાચ એક SMS મોકલવામાં આવી શકે છે. સંદેશ અને ડેટા શુલ્ક લાગુ થઈ શકે છે. “%1$s” ટૅપ કરીને, તમે સૂચવી રહ્યાં છો કે તમે અમારી %2$s અને %3$sને સ્વીકારો છો. SMS મોકલવામાં આવી શકે છે. સંદેશ અને ડેટા શુલ્ક લાગુ થઈ શકે છે. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + પ્રમાણીકરણ ભૂલ + ફરી પ્રયાસ કરો + વધારાની ચકાસણી જરૂરી છે. કૃપા કરીને મલ્ટિ-ફૅક્ટર પ્રમાણીકરણ પૂર્ણ કરો. + એકાઉન્ટ લિંક કરવાની જરૂર છે. કૃપા કરીને કોઈ અલગ સાઇન ઇન પદ્ધતિ અજમાવો. + પ્રમાણીકરણ રદ કરવામાં આવ્યું. તમે તૈયાર હો ત્યારે ફરી પ્રયાસ કરો. પ્રમાણીકરણ પદ્ધતિ પસંદ કરો @@ -166,7 +166,7 @@ ચકાસણી ઇમેઇલ ફરી મોકલો ગુપ્ત કી સાઇન આઉટ - તરીકે સાઇન ઇન કર્યું + %1$s તરીકે સાઇન ઇન કર્યું છોડો એક અલગ પદ્ધતિ વાપરો ચકાસણી કોડ @@ -176,4 +176,17 @@ મલ્ટિ-ફેક્ટર પ્રમાણીકરણ હાલમાં અક્ષમ છે + તે ઇમેઇલ અથવા પાસવર્ડ સાચો નથી + તે ચકાસણી સત્ર હવે માન્ય નથી. નવા કોડની વિનંતી કરો. + ફોનની ચકાસણી પૂર્ણ થઈ નથી. ફરી પ્રયાસ કરો. + તે ઓળખપત્રો કોઈ અલગ એકાઉન્ટનાં છે. + તે ફોન નંબર આ એકાઉન્ટ પર ચકાસણી માટે સેટ કરેલો નથી. + તમારું સાઇન ઇન સત્ર સમાપ્ત થઈ ગયું છે. ચાલુ રાખવા માટે ફરી સાઇન ઇન કરો. + તે લિંક હવે માન્ય નથી. નવી લિંકની વિનંતી કરો. + તમે આગળ વધો તે પહેલાં તમારું ઇમેઇલ ઍડ્રેસ ચકાસો. + તે ચકાસણી પદ્ધતિ આ એકાઉન્ટ પર પહેલેથી જ સેટ કરેલી છે. + તમે આ એકાઉન્ટ પર ચકાસણી પદ્ધતિઓની મર્યાદા પર પહોંચી ગયા છો. + તમારો પાસવર્ડ જરૂરિયાતો પૂરી કરતો નથી. બીજો પાસવર્ડ અજમાવો. + પાસવર્ડ ઘણો લાંબો છે. મહત્તમ લંબાઈ %1$d છે. + આ એકાઉન્ટ માટે કોઈ પાસકી મળી નથી. બીજી રીતે સાઇન ઇન કરો. diff --git a/auth/src/main/res/values-hi/strings.xml b/auth/src/main/res/values-hi/strings.xml index 45173de19c..4459b3fad5 100755 --- a/auth/src/main/res/values-hi/strings.xml +++ b/auth/src/main/res/values-hi/strings.xml @@ -100,15 +100,15 @@ फ़ोन नंबर की अपने आप पुष्टि की गई कोड फिर से भेजें फ़ोन नंबर की पुष्टि करें - Use a different phone number + दूसरे फ़ोन नंबर का इस्तेमाल करें “%1$s” पर टैप करने पर, एक मैसेज (एसएमएस) भेजा जा सकता है. मैसेज और डेटा दरें लागू हो सकती हैं. “%1$s” पर टैप करके, आप यह बताते हैं कि आप हमारी %2$s और %3$s को मंज़ूर करते हैं. एक मैसेज (एसएमएस) भेजा जा सकता है. मैसेज और डेटा दरें लागू हो सकती हैं. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + प्रमाणीकरण में गड़बड़ी + फिर से कोशिश करें + अतिरिक्त सत्यापन ज़रूरी है. कृपया मल्टी-फ़ैक्टर प्रमाणीकरण पूरा करें. + खाते को लिंक करना ज़रूरी है. कृपया प्रवेश करने का कोई दूसरा तरीका आज़माएँ. + प्रमाणीकरण रद्द कर दिया गया. तैयार होने पर फिर से कोशिश करें. प्रमाणीकरण विधि चुनें @@ -166,7 +166,7 @@ सत्यापन ईमेल फिर से भेजें गुप्त कुंजी साइन आउट - इस रूप में साइन इन किया + %1$s के रूप में साइन इन किया छोड़ें एक अलग विधि उपयोग करें सत्यापन कोड @@ -176,4 +176,17 @@ मल्टी-फैक्टर प्रमाणीकरण वर्तमान में अक्षम है + वह ईमेल या पासवर्ड सही नहीं है + वह पुष्टि करने वाला सेशन अब मान्य नहीं है. नया कोड पाने का अनुरोध करें. + फ़ोन की पुष्टि पूरी नहीं हुई. कृपया फिर से कोशिश करें. + वे क्रेडेंशियल किसी दूसरे खाते के हैं. + वह फ़ोन नंबर इस खाते पर पुष्टि के लिए सेट अप नहीं है. + आपका साइन इन सेशन खत्म हो गया है. जारी रखने के लिए फिर से साइन इन करें. + वह लिंक अब मान्य नहीं है. नया लिंक पाने का अनुरोध करें. + आगे बढ़ने से पहले अपने ईमेल पते की पुष्टि करें. + पुष्टि करने का वह तरीका इस खाते पर पहले से सेट अप है. + आपने इस खाते पर पुष्टि करने के तरीकों की सीमा पूरी कर ली है. + आपका पासवर्ड ज़रूरी शर्तें पूरी नहीं करता. कोई दूसरा पासवर्ड आज़माएं. + पासवर्ड बहुत लंबा है. ज़्यादा से ज़्यादा लंबाई %1$d है. + इस खाते के लिए कोई पासकी नहीं मिली. किसी दूसरे तरीके से साइन इन करें. diff --git a/auth/src/main/res/values-hr/strings.xml b/auth/src/main/res/values-hr/strings.xml index b6fef2393d..cdf9241fd3 100755 --- a/auth/src/main/res/values-hr/strings.xml +++ b/auth/src/main/res/values-hr/strings.xml @@ -100,7 +100,7 @@ Telefonski je broj automatski potvrđen Ponovo pošalji kôd Potvrda telefonskog broja - Use a different phone number + Upotrijebi drugi telefonski broj Dodirivanje gumba “%1$s” može dovesti do slanja SMS poruke. Mogu se primijeniti naknade za slanje poruka i podatkovni promet. Ako dodirnete "%1$s", potvrđujete da prihvaćate odredbe koje sadrže %2$s i %3$s. Možda ćemo vam poslati SMS. Moguća je naplata poruke i podatkovnog prometa. Greška provjere identiteta @@ -165,7 +165,7 @@ Ponovno pošalji e-poštu za provjeru Tajni ključ Odjava - Prijavljen kao + Prijavljen kao %1$s Preskoči Koristi drugu metodu Kod za provjeru @@ -175,4 +175,17 @@ Višefaktorska autentifikacija trenutno je onemogućena + Ta e-adresa ili zaporka nije točna + Ta sesija potvrde više nije važeća. Zatražite novi kôd. + Potvrda telefonskog broja nije dovršena. Pokušajte ponovno. + Te vjerodajnice pripadaju drugom računu. + Taj telefonski broj nije postavljen za potvrdu na ovom računu. + Vaša je sesija prijave istekla. Prijavite se ponovno da biste nastavili. + Ta veza više nije važeća. Zatražite novu. + Potvrdite svoju e-adresu prije nego što nastavite. + Taj je način potvrde već postavljen na ovom računu. + Dosegnuli ste ograničenje broja načina potvrde na ovom računu. + Vaša zaporka ne ispunjava uvjete. Pokušajte s drugom. + Zaporka je predugačka. Najveća duljina je %1$d. + Nismo pronašli pristupni ključ za ovaj račun. Prijavite se na drugi način. diff --git a/auth/src/main/res/values-hu/strings.xml b/auth/src/main/res/values-hu/strings.xml index 90e888d74f..ed0ae3bb53 100755 --- a/auth/src/main/res/values-hu/strings.xml +++ b/auth/src/main/res/values-hu/strings.xml @@ -100,7 +100,7 @@ A telefonszám automatikusan ellenőrizve Kód újraküldése Telefonszám igazolása - Use a different phone number + Másik telefonszám használata Ha a(z) „%1$s” gombra koppint, a rendszer SMS-t küldhet Önnek. A szolgáltató ezért üzenet- és adatforgalmi díjat számíthat fel. A(z) „%1$s” gombra való koppintással elfogadja a következő dokumentumokat: %2$s és %3$s. A rendszer SMS-t küldhet Önnek. A szolgáltató ezért üzenet- és adatforgalmi díjat számíthat fel. Hitelesítési hiba @@ -165,7 +165,7 @@ Ellenőrző e-mail újraküldése Titkos kulcs Kijelentkezés - Bejelentkezve mint + Bejelentkezve mint %1$s Kihagyás Másik módszer használata Ellenőrző kód @@ -175,4 +175,17 @@ A többfaktoros hitelesítés jelenleg le van tiltva + Az e-mail-cím vagy a jelszó nem helyes + Ez az ellenőrzési munkamenet már nem érvényes. Kérjen új kódot. + A telefonszám ellenőrzése nem fejeződött be. Kérjük, próbálja újra. + Ezek a hitelesítő adatok egy másik fiókhoz tartoznak. + Ez a telefonszám nincs beállítva ellenőrzésre ebben a fiókban. + A bejelentkezési munkamenet lejárt. A folytatáshoz jelentkezzen be újra. + Ez a link már nem érvényes. Kérjen újat. + A folytatás előtt erősítse meg az e-mail-címét. + Ez az ellenőrzési módszer már be van állítva ebben a fiókban. + Elérte az ebben a fiókban beállítható ellenőrzési módszerek felső határát. + A jelszava nem felel meg a követelményeknek. Próbáljon meg egy másikat. + A jelszó túl hosszú. A maximális hossz %1$d. + Nem találtunk azonosítókulcsot ehhez a fiókhoz. Jelentkezzen be másik módon. diff --git a/auth/src/main/res/values-in/strings.xml b/auth/src/main/res/values-in/strings.xml index ef5093027a..d71cc13b0e 100755 --- a/auth/src/main/res/values-in/strings.xml +++ b/auth/src/main/res/values-in/strings.xml @@ -10,7 +10,7 @@ Twitter GitHub Ponsel - Email + Alamat email Login dengan Google Login dengan Google Login dengan Facebook @@ -31,7 +31,7 @@ Login dengan Yahoo Login dengan Yahoo Berikutnya - Email + Alamat email Nomor Telepon Negara Pilih negara @@ -100,15 +100,15 @@ Nomor telepon terverifikasi secara otomatis Kirim Ulang Kode Verifikasi Nomor Telepon - Use a different phone number + Gunakan nomor telepon lain Dengan mengetuk “%1$s\", SMS mungkin akan dikirim. Mungkin dikenakan biaya pesan & data. Dengan mengetuk “%1$s”, Anda menyatakan bahwa Anda menyetujui %2$s dan %3$s kami. SMS mungkin akan dikirim. Mungkin dikenakan biaya pesan & data. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Error Autentikasi + Coba lagi + Verifikasi tambahan diperlukan. Harap selesaikan autentikasi multi-faktor. + Akun perlu ditautkan. Harap coba metode login yang lain. + Autentikasi dibatalkan. Harap coba lagi saat Anda siap. Pilih Metode Autentikasi @@ -166,7 +166,7 @@ Kirim ulang email verifikasi Kunci rahasia Keluar - Masuk sebagai + Masuk sebagai %1$s Lewati Gunakan metode lain Kode verifikasi @@ -176,4 +176,17 @@ Autentikasi multifaktor saat ini dinonaktifkan + Email atau sandi tersebut salah + Sesi verifikasi tersebut sudah tidak valid. Minta kode baru. + Verifikasi telepon tidak selesai. Harap coba lagi. + Kredensial tersebut milik akun lain. + Nomor telepon tersebut tidak disiapkan untuk verifikasi di akun ini. + Sesi login Anda sudah berakhir. Login lagi untuk melanjutkan. + Link tersebut sudah tidak valid. Minta link baru. + Verifikasi alamat email Anda sebelum melanjutkan. + Metode verifikasi tersebut sudah disiapkan di akun ini. + Anda telah mencapai batas metode verifikasi di akun ini. + Sandi Anda tidak memenuhi persyaratan. Coba sandi lain. + Sandi terlalu panjang. Panjang maksimumnya adalah %1$d. + Kami tidak menemukan kunci sandi untuk akun ini. Login dengan cara lain. diff --git a/auth/src/main/res/values-it/strings.xml b/auth/src/main/res/values-it/strings.xml index da7f3a1a6d..7641358e5b 100755 --- a/auth/src/main/res/values-it/strings.xml +++ b/auth/src/main/res/values-it/strings.xml @@ -47,7 +47,7 @@ Nome e cognome Salva Registrazione in corso… - La password non è abbastanza efficace. Utilizza almeno %1$d carattere e una combinazione di lettere e numeri. La password non è abbastanza efficace. Utilizza almeno %1$d caratteri e una combinazione di lettere e numeri. + La password non è abbastanza efficace. Utilizza almeno %1$d carattere e una combinazione di lettere e numeri. La password non è abbastanza efficace. Utilizza almeno %1$d di caratteri e una combinazione di lettere e numeri. La password non è abbastanza efficace. Utilizza almeno %1$d caratteri e una combinazione di lettere e numeri. Registrazione account di posta elettronica non riuscita Termini di servizio Norme sulla privacy @@ -100,7 +100,7 @@ Numero di telefono verificato automaticamente Invia di nuovo il codice Verifica numero di telefono - Use a different phone number + Usa un altro numero di telefono Se tocchi “%1$s”, è possibile che venga inviato un SMS. Potrebbero essere applicate le tariffe per l\'invio dei messaggi e per il traffico dati. Se tocchi "%1$s", accetti i nostri %2$s e le nostre %3$s. È possibile che venga inviato un SMS. Potrebbero essere applicate le tariffe per l\'invio dei messaggi e per il traffico dati. Errore di autenticazione @@ -165,7 +165,7 @@ Invia nuovamente email di verifica Chiave segreta Esci - Connesso come + Connesso come %1$s Salta Usa un metodo diverso Codice di verifica @@ -175,4 +175,17 @@ L\'autenticazione a più fattori è attualmente disabilitata + L\'email o la password non sono corretti + Questa sessione di verifica non è più valida. Richiedi un nuovo codice. + La verifica del telefono non è stata completata. Riprova. + Queste credenziali appartengono a un altro account. + Questo numero di telefono non è configurato per la verifica su questo account. + La tua sessione di accesso è scaduta. Accedi di nuovo per continuare. + Questo link non è più valido. Richiedine uno nuovo. + Verifica il tuo indirizzo email prima di continuare. + Questo metodo di verifica è già configurato su questo account. + Hai raggiunto il limite di metodi di verifica per questo account. + La tua password non soddisfa i requisiti. Provane un\'altra. + La password è troppo lunga. La lunghezza massima è %1$d. + Non abbiamo trovato una passkey per questo account. Accedi in un altro modo. diff --git a/auth/src/main/res/values-iw/strings.xml b/auth/src/main/res/values-iw/strings.xml index af4bc1c8c9..a95184ee03 100755 --- a/auth/src/main/res/values-iw/strings.xml +++ b/auth/src/main/res/values-iw/strings.xml @@ -100,15 +100,15 @@ מספר הטלפון אומת באופן אוטומטי שלח קוד חדש אמת את מספר הטלפון - Use a different phone number + שימוש במספר טלפון אחר הקשה על “%1$s” עשויה לגרום לשליחה של הודעת SMS. ייתכן שיחולו תעריפי הודעות והעברת נתונים. הקשה על “%1$s”, תפורש כהסכמתך ל%2$s ול%3$s. ייתכן שתישלח הודעת SMS. ייתכנו חיובים בגין שליחת הודעות ושימוש בנתונים. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + שגיאת אימות + נסה שוב + נדרש אימות נוסף. יש להשלים אימות רב-שלבי. + יש לקשר את החשבון. יש לנסות שיטת כניסה אחרת. + האימות בוטל. יש לנסות שוב כשתהיו מוכנים. בחר שיטת אימות @@ -166,7 +166,7 @@ שלח שוב אימייל אימות מפתח סודי התנתק - מחובר בתור + מחובר בתור %1$s דלג השתמש בשיטה אחרת קוד אימות @@ -176,4 +176,17 @@ אימות רב-גורמי מושבת כעת + האימייל או הסיסמה שגויים + הפעלת האימות הזו כבר לא בתוקף. יש לבקש קוד חדש. + אימות הטלפון לא הושלם. יש לנסות שוב. + פרטי הכניסה האלה שייכים לחשבון אחר. + מספר הטלפון הזה לא מוגדר לאימות בחשבון הזה. + הפעלת הכניסה שלך פגה. יש להיכנס שוב כדי להמשיך. + הקישור הזה כבר לא בתוקף. יש לבקש קישור חדש. + יש לאמת את כתובת האימייל שלך לפני שממשיכים. + שיטת האימות הזו כבר מוגדרת בחשבון הזה. + הגעת למגבלה של שיטות אימות בחשבון הזה. + הסיסמה שלך לא עומדת בדרישות. אפשר לנסות סיסמה אחרת. + הסיסמה ארוכה מדי. האורך המקסימלי הוא %1$d. + לא נמצא מפתח גישה לחשבון הזה. אפשר להיכנס בדרך אחרת. diff --git a/auth/src/main/res/values-ja/strings.xml b/auth/src/main/res/values-ja/strings.xml index d540d4edae..4eb0dd979d 100755 --- a/auth/src/main/res/values-ja/strings.xml +++ b/auth/src/main/res/values-ja/strings.xml @@ -100,7 +100,7 @@ 電話番号は自動的に確認されました コードを再送信 電話番号を確認 - Use a different phone number + 別の電話番号を使用 [%1$s] をタップすると、SMS が送信されます。データ通信料がかかることがあります。 [%1$s] をタップすると、%2$s と %3$s に同意したことになり、SMS が送信されます。データ通信料がかかることがあります。 認証エラー @@ -165,7 +165,7 @@ 確認メールを再送信 シークレットキー ログアウト - ログイン中 + %1$s としてログイン中 スキップ 別の方法を使用 確認コード @@ -175,4 +175,17 @@ 多要素認証は現在無効になっています + メールアドレスまたはパスワードが正しくありません + この確認セッションは無効になりました。新しいコードをリクエストしてください。 + 電話番号の確認が完了しませんでした。もう一度お試しください。 + この認証情報は別のアカウントのものです。 + この電話番号は、このアカウントの確認方法として設定されていません。 + ログインセッションの有効期限が切れました。続行するには、もう一度ログインしてください。 + このリンクは無効になりました。新しいリンクをリクエストしてください。 + 続行する前にメールアドレスを確認してください。 + この確認方法はこのアカウントですでに設定されています。 + このアカウントで設定できる確認方法の上限に達しました。 + パスワードが要件を満たしていません。別のパスワードをお試しください。 + パスワードが長すぎます。最大文字数は %1$d です。 + このアカウントのパスキーが見つかりませんでした。別の方法でログインしてください。 diff --git a/auth/src/main/res/values-kn/strings.xml b/auth/src/main/res/values-kn/strings.xml index 2be1d35d14..0084140ae6 100755 --- a/auth/src/main/res/values-kn/strings.xml +++ b/auth/src/main/res/values-kn/strings.xml @@ -100,15 +100,15 @@ ಫೋನ್ ಸಂಖ್ಯೆಯನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಪರಿಶೀಲಿಸಲಾಗಿದೆ ಕೋಡ್ ಪುನಃ ಕಳುಹಿಸಿ ಫೋನ್ ಸಂಖ್ಯೆಯನ್ನು ಪರಿಶೀಲಿಸಿ - Use a different phone number + ಬೇರೆ ಫೋನ್ ಸಂಖ್ಯೆಯನ್ನು ಬಳಸಿ “%1$s” ಅನ್ನು ಟ್ಯಾಪ್ ಮಾಡುವ ಮೂಲಕ, ಎಸ್‌ಎಂಎಸ್‌ ಅನ್ನು ಕಳುಹಿಸಬಹುದಾಗಿದೆ. ಸಂದೇಶ ಮತ್ತು ಡೇಟಾ ದರಗಳು ಅನ್ವಯಿಸಬಹುದು. “%1$s” ಅನ್ನು ಟ್ಯಾಪ್ ಮಾಡುವ ಮೂಲಕ, ನೀವು ನಮ್ಮ %2$s ಮತ್ತು %3$s ಸ್ವೀಕರಿಸುತ್ತೀರಿ ಎಂದು ನೀವು ಸೂಚಿಸುತ್ತಿರುವಿರಿ. ಎಸ್‌ಎಂಎಸ್‌ ಅನ್ನು ಕಳುಹಿಸಬಹುದಾಗಿದೆ. ಸಂದೇಶ ಮತ್ತು ಡೇಟಾ ದರಗಳು ಅನ್ವಯಿಸಬಹುದು. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + ಪ್ರಮಾಣೀಕರಣ ದೋಷ + ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ + ಹೆಚ್ಚುವರಿ ಪರಿಶೀಲನೆ ಅಗತ್ಯವಿದೆ. ದಯವಿಟ್ಟು ಬಹು-ಅಂಶ ಪ್ರಮಾಣೀಕರಣವನ್ನು ಪೂರ್ಣಗೊಳಿಸಿ. + ಖಾತೆಯನ್ನು ಲಿಂಕ್ ಮಾಡಬೇಕಾಗಿದೆ. ದಯವಿಟ್ಟು ಬೇರೆ ಸೈನ್ ಇನ್ ವಿಧಾನವನ್ನು ಪ್ರಯತ್ನಿಸಿ. + ಪ್ರಮಾಣೀಕರಣವನ್ನು ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ. ನೀವು ಸಿದ್ಧರಾದಾಗ ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ. ದೃಢೀಕರಣ ವಿಧಾನವನ್ನು ಆಯ್ಕೆಮಾಡಿ @@ -166,7 +166,7 @@ ಪರಿಶೀಲನೆ ಇಮೇಲ್ ಮರುಕಳುಹಿಸಿ ರಹಸ್ಯ ಕೀ ಸೈನ್ ಔಟ್ - ಹೀಗೆ ಸೈನ್ ಇನ್ ಆಗಿದ್ದೀರಿ + %1$s ಆಗಿ ಸೈನ್ ಇನ್ ಆಗಿದ್ದೀರಿ ಸ್ಕಿಪ್ ಮಾಡಿ ವಿಭಿನ್ನ ವಿಧಾನವನ್ನು ಬಳಸಿ ಪರಿಶೀಲನೆ ಕೋಡ್ @@ -176,4 +176,17 @@ ಮಲ್ಟಿ-ಫ್ಯಾಕ್ಟರ್ ದೃಢೀಕರಣವು ಪ್ರಸ್ತುತ ನಿಷ್ಕ್ರಿಯಗೊಂಡಿದೆ + ಆ ಇಮೇಲ್ ಅಥವಾ ಪಾಸ್‌ವರ್ಡ್ ಸರಿಯಾಗಿಲ್ಲ + ಆ ಪರಿಶೀಲನಾ ಸೆಶನ್ ಇನ್ನು ಮುಂದೆ ಮಾನ್ಯವಾಗಿಲ್ಲ. ಹೊಸ ಕೋಡ್‌ಗೆ ವಿನಂತಿಸಿ. + ಫೋನ್ ಪರಿಶೀಲನೆ ಪೂರ್ಣಗೊಂಡಿಲ್ಲ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ. + ಆ ರುಜುವಾತುಗಳು ಬೇರೆ ಖಾತೆಗೆ ಸೇರಿವೆ. + ಆ ಫೋನ್ ಸಂಖ್ಯೆಯನ್ನು ಈ ಖಾತೆಯಲ್ಲಿ ಪರಿಶೀಲನೆಗಾಗಿ ಹೊಂದಿಸಲಾಗಿಲ್ಲ. + ನಿಮ್ಮ ಸೈನ್ ಇನ್ ಸೆಶನ್‌ನ ಅವಧಿ ಮುಗಿದಿದೆ. ಮುಂದುವರಿಸಲು ಮತ್ತೆ ಸೈನ್ ಇನ್ ಮಾಡಿ. + ಆ ಲಿಂಕ್ ಇನ್ನು ಮುಂದೆ ಮಾನ್ಯವಾಗಿಲ್ಲ. ಹೊಸ ಲಿಂಕ್‌ಗೆ ವಿನಂತಿಸಿ. + ಮುಂದುವರಿಯುವ ಮೊದಲು ನಿಮ್ಮ ಇಮೇಲ್ ವಿಳಾಸವನ್ನು ಪರಿಶೀಲಿಸಿ. + ಆ ಪರಿಶೀಲನಾ ವಿಧಾನವನ್ನು ಈ ಖಾತೆಯಲ್ಲಿ ಈಗಾಗಲೇ ಹೊಂದಿಸಲಾಗಿದೆ. + ಈ ಖಾತೆಯಲ್ಲಿನ ಪರಿಶೀಲನಾ ವಿಧಾನಗಳ ಮಿತಿಯನ್ನು ನೀವು ತಲುಪಿದ್ದೀರಿ. + ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯತೆಗಳನ್ನು ಪೂರೈಸುತ್ತಿಲ್ಲ. ಬೇರೊಂದನ್ನು ಪ್ರಯತ್ನಿಸಿ. + ಪಾಸ್‌ವರ್ಡ್ ತುಂಬಾ ಉದ್ದವಾಗಿದೆ. ಗರಿಷ್ಠ ಉದ್ದ %1$d ಆಗಿದೆ. + ಈ ಖಾತೆಗೆ ಪಾಸ್‌ಕೀ ಕಂಡುಬಂದಿಲ್ಲ. ಬೇರೆ ವಿಧಾನದಲ್ಲಿ ಸೈನ್ ಇನ್ ಮಾಡಿ. diff --git a/auth/src/main/res/values-ko/strings.xml b/auth/src/main/res/values-ko/strings.xml index abcbfc82e9..881f7bad41 100755 --- a/auth/src/main/res/values-ko/strings.xml +++ b/auth/src/main/res/values-ko/strings.xml @@ -100,7 +100,7 @@ 전화번호가 자동으로 확인되었습니다. 코드 재전송 전화번호 인증 - Use a different phone number + 다른 전화번호 사용 “%1$s” 버튼을 탭하면 SMS가 발송될 수 있으며, 메시지 및 데이터 요금이 부과될 수 있습니다. 인증 오류 다시 시도 @@ -164,7 +164,7 @@ 확인 이메일 다시 보내기 비밀 키 로그아웃 - 로그인 상태 + %1$s(으)로 로그인 상태 건너뛰기 다른 방법 사용 확인 코드 @@ -174,4 +174,17 @@ 다단계 인증이 현재 비활성화되어 있습니다 + 이메일 또는 비밀번호가 올바르지 않습니다. + 이 인증 세션은 더 이상 유효하지 않습니다. 새 코드를 요청하세요. + 전화번호 인증이 완료되지 않았습니다. 다시 시도하세요. + 이 사용자 인증 정보는 다른 계정에 속해 있습니다. + 이 전화번호는 이 계정의 인증 수단으로 설정되어 있지 않습니다. + 로그인 세션이 만료되었습니다. 계속하려면 다시 로그인하세요. + 이 링크는 더 이상 유효하지 않습니다. 새 링크를 요청하세요. + 계속하기 전에 이메일 주소를 인증하세요. + 이 인증 수단은 이 계정에 이미 설정되어 있습니다. + 이 계정에서 설정할 수 있는 인증 수단 한도에 도달했습니다. + 비밀번호가 요건을 충족하지 않습니다. 다른 비밀번호를 사용해 보세요. + 비밀번호가 너무 깁니다. 최대 길이는 %1$d입니다. + 이 계정의 패스키를 찾을 수 없습니다. 다른 방법으로 로그인하세요. diff --git a/auth/src/main/res/values-ln/strings.xml b/auth/src/main/res/values-ln/strings.xml index 1931ef3766..f03abf8090 100755 --- a/auth/src/main/res/values-ln/strings.xml +++ b/auth/src/main/res/values-ln/strings.xml @@ -100,15 +100,15 @@ Numéro de téléphone validé automatiquement Renvoyer le code Valider le numéro de téléphone - Use a different phone number + Salelá nimero mosusu ya telefone En appuyant sur “%1$s”, vous déclencherez peut-être l\'envoi d\'un SMS. Des frais de messages et de données peuvent être facturés. En appuyant sur “%1$s”, vous acceptez les %2$s et les %3$s. Vous déclencherez peut-être l\'envoi d\'un SMS. Des frais de messages et de données peuvent être facturés. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Libunga ya bondimi + Meka lisusu + Bondimi mosusu esengeli. Tosɛngi osilisa bondimisami ya makambo mingi. + Konte esengeli kokangisama. Tosɛngi omeka lolenge mosusu ya kokɔta. + Bondimi e-annulé. Tosɛngi omeka lisusu ntango okozala pene. Pona lolenge ya bondimi @@ -166,7 +166,7 @@ Tinda lisusu e-mail ya vérification Clé secrète Kobima - Okoti lokola + Okoti lokola %1$s Leka Salelá méthode mosusu Code ya vérification @@ -176,4 +176,17 @@ Bondimisami ya makambo mingi ezali sikoyo te + Email to password wana ezali sembo te + Session wana ya bondimi ezali lisusu na ntina te. Sɛnga kode ya sika. + Bondimi ya telefone esili te. Meka lisusu. + Ba code wana ya kokɔta ezali ya konte mosusu. + Nimero wana ya telefone ebongisami te mpo na bondimi na konte oyo. + Session na yo ya kokɔta esili. Kɔta lisusu mpo na kokoba. + Lien wana ezali lisusu malamu te. Sɛnga lien ya sika. + Ndimisa adrɛsɛ na yo ya email liboso ya kokoba. + Lolenge wana ya bondimi ebongisami déjà na konte oyo. + Okómi na ndelo ya balolenge ya bondimi na konte oyo. + Mot de passe na yo ekokisi te makambo esengeli. Meka mosusu. + Mot de passe ezali molayi mingi. Molayi ya likolo ezali %1$d. + Tomonaki te clé ya kokɔta mpo na compte oyo. Kɔta na ndenge mosusu. diff --git a/auth/src/main/res/values-lt/strings.xml b/auth/src/main/res/values-lt/strings.xml index b68b5aabb2..b21a3a59a0 100755 --- a/auth/src/main/res/values-lt/strings.xml +++ b/auth/src/main/res/values-lt/strings.xml @@ -100,15 +100,15 @@ Telefono numeris patvirtintas automatiškai Siųsti kodą iš naujo Patvirtinti telefono numerį - Use a different phone number + Naudoti kitą telefono numerį Palietus „%1$s“ gali būti išsiųstas SMS pranešimas. Gali būti taikomi pranešimų ir duomenų įkainiai. Paliesdami „%1$s“ nurodote, kad sutinkate su %2$s ir %3$s. Gali būti išsiųstas SMS pranešimas, taip pat – taikomi pranešimų ir duomenų įkainiai. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Autentifikavimo klaida + Bandykite dar kartą + Reikalingas papildomas patvirtinimas. Užbaikite kelių veiksnių autentifikavimą. + Paskyrą reikia susieti. Išbandykite kitą prisijungimo būdą. + Autentifikavimas buvo atšauktas. Kai būsite pasirengę, bandykite dar kartą. Pasirinkite autentifikavimo metodą @@ -166,7 +166,7 @@ Siųsti patvirtinimo el. laišką iš naujo Slaptasis raktas Atsijungti - Prisijungta kaip + Prisijungta kaip %1$s Praleisti Naudoti kitą metodą Patvirtinimo kodas @@ -176,4 +176,17 @@ Daugiafaktoris tapatybės nustatymas šiuo metu išjungtas + Šis el. pašto adresas arba slaptažodis neteisingas + Šis patvirtinimo seansas nebegalioja. Paprašykite naujo kodo. + Telefono numerio patvirtinimas nebuvo baigtas. Bandykite dar kartą. + Šie prisijungimo duomenys priklauso kitai paskyrai. + Šis telefono numeris nenustatytas kaip patvirtinimo būdas šioje paskyroje. + Jūsų prisijungimo seansas baigėsi. Norėdami tęsti, prisijunkite dar kartą. + Ši nuoroda nebegalioja. Paprašykite naujos. + Prieš tęsdami patvirtinkite savo el. pašto adresą. + Šis patvirtinimo būdas šioje paskyroje jau nustatytas. + Pasiekėte šios paskyros patvirtinimo būdų ribą. + Jūsų slaptažodis neatitinka reikalavimų. Išbandykite kitą. + Slaptažodis per ilgas. Didžiausias ilgis yra %1$d. + Nepavyko rasti šios paskyros prieigos rakto. Prisijunkite kitu būdu. diff --git a/auth/src/main/res/values-lv/strings.xml b/auth/src/main/res/values-lv/strings.xml index fb0f2ca26e..3489bf767d 100755 --- a/auth/src/main/res/values-lv/strings.xml +++ b/auth/src/main/res/values-lv/strings.xml @@ -100,15 +100,15 @@ Tālruņa numurs tika automātiski verificēts Vēlreiz nosūtīt kodu Verificēt tālruņa numuru - Use a different phone number + Izmantot citu tālruņa numuru Pieskaroties pogai %1$s, var tikt nosūtīta īsziņa. Var tikt piemērota maksa par ziņojumiem un datu pārsūtīšanu. Pieskaroties pogai “%1$s”, jūs norādāt, ka piekrītat šādiem dokumentiem: %2$s un %3$s. Var tikt nosūtīta īsziņa. Var tikt piemērota maksa par ziņojumiem un datu pārsūtīšanu. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Autentifikācijas kļūda + Mēģiniet vēlreiz + Nepieciešama papildu verifikācija. Lūdzu, pabeidziet vairāku faktoru autentifikāciju. + Konts ir jāsaista. Lūdzu, izmēģiniet citu pierakstīšanās metodi. + Autentifikācija tika atcelta. Lūdzu, mēģiniet vēlreiz, kad būsiet gatavs. Izvēlieties autentifikācijas metodi @@ -166,7 +166,7 @@ Atkārtoti nosūtīt verifikācijas e-pastu Slepenā atslēga Izrakstīties - Pierakstījies kā + Pierakstījies kā %1$s Izlaist Izmantot citu metodi Verifikācijas kods @@ -176,4 +176,17 @@ Daudzfaktoru autentifikācija pašlaik ir atspējota + E-pasta adrese vai parole nav pareiza + Šī verifikācijas sesija vairs nav derīga. Pieprasiet jaunu kodu. + Tālruņa numura verifikācija netika pabeigta. Lūdzu, mēģiniet vēlreiz. + Šie akreditācijas dati pieder citam kontam. + Šis tālruņa numurs šajā kontā nav iestatīts verifikācijai. + Jūsu pierakstīšanās sesijai ir beidzies derīguma termiņš. Lai turpinātu, piesakieties vēlreiz. + Šī saite vairs nav derīga. Pieprasiet jaunu. + Pirms turpināt, verificējiet savu e-pasta adresi. + Šī verifikācijas metode šajā kontā jau ir iestatīta. + Jūs esat sasniedzis šī konta verifikācijas metožu ierobežojumu. + Jūsu parole neatbilst prasībām. Mēģiniet citu. + Parole ir pārāk gara. Maksimālais garums ir %1$d. + Neatradām šī konta piekļuves atslēgu. Piesakieties citā veidā. diff --git a/auth/src/main/res/values-mo/strings.xml b/auth/src/main/res/values-mo/strings.xml index f96444e777..c1b527994d 100755 --- a/auth/src/main/res/values-mo/strings.xml +++ b/auth/src/main/res/values-mo/strings.xml @@ -100,15 +100,15 @@ Numărul de telefon este verificat automat Retrimiteți codul Confirmați numărul de telefon - Use a different phone number + Folosiți alt număr de telefon Dacă atingeți „%1$s”, poate fi trimis un SMS. Se pot aplica tarife pentru mesaje și date. Dacă atingeți „%1$s”, sunteți de acord cu %2$s și cu %3$s. Poate fi trimis un SMS. Se pot aplica tarife pentru mesaje și date. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Eroare de autentificare + Încercați din nou + Este necesară o verificare suplimentară. Finalizați autentificarea cu mai mulți factori. + Contul trebuie asociat. Încercați altă metodă de conectare. + Autentificarea a fost anulată. Încercați din nou când sunteți gata. Alegeți metoda de autentificare @@ -166,7 +166,7 @@ Retrimite e-mailul de verificare Cheie secretă Deconectare - Conectat ca + Conectat ca %1$s Omite Folosiți o altă metodă Cod de verificare @@ -176,4 +176,17 @@ Autentificarea cu mai mulți factori este dezactivată în prezent + Adresa de e-mail sau parola nu este corectă + Sesiunea de verificare nu mai este validă. Solicitați un cod nou. + Verificarea numărului de telefon nu s-a finalizat. Încercați din nou. + Aceste date de conectare aparțin altui cont. + Acest număr de telefon nu este configurat pentru verificare în acest cont. + Sesiunea de conectare a expirat. Conectați-vă din nou pentru a continua. + Acest link nu mai este valid. Solicitați unul nou. + Confirmați adresa de e-mail înainte de a continua. + Această metodă de verificare este deja configurată în acest cont. + Ați atins limita de metode de verificare pentru acest cont. + Parola nu îndeplinește cerințele. Încercați alta. + Parola este prea lungă. Lungimea maximă este %1$d. + Nu am găsit o cheie de acces pentru acest cont. Conectați-vă în alt mod. diff --git a/auth/src/main/res/values-mr/strings.xml b/auth/src/main/res/values-mr/strings.xml index 38d2a9c8da..eb80237d01 100755 --- a/auth/src/main/res/values-mr/strings.xml +++ b/auth/src/main/res/values-mr/strings.xml @@ -100,15 +100,15 @@ फोन नंबरची अपोआप पडताळणी केली आहे कोड पुन्हा पाठवा फोन नंबरची पडताळणी करा - Use a different phone number + वेगळा फोन नंबर वापरा “%1$s“ वर टॅप केल्याने, एक एसएमएस पाठवला जाऊ शकतो. मेसेज आणि डेटा शुल्क लागू होऊ शकते. “%1$s” वर टॅप करून, तुम्ही सूचित करता की तुम्ही आमचे %2$s आणि %3$s स्वीकारता. एसएमएस पाठवला जाऊ शकतो. मेसेज आणि डेटा दर लागू केले जाऊ शकते. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + प्रमाणीकरण एरर + पुन्हा प्रयत्न करा + अतिरिक्त पडताळणी आवश्यक आहे. कृपया मल्टी-फॅक्टर प्रमाणीकरण पूर्ण करा. + खाते लिंक करणे आवश्यक आहे. कृपया वेगळी साइन इन पद्धत वापरून पहा. + प्रमाणीकरण रद्द केले गेले. तुम्ही तयार असाल तेव्हा कृपया पुन्हा प्रयत्न करा. प्रमाणीकरण पद्धत निवडा @@ -166,7 +166,7 @@ सत्यापन ईमेल पुन्हा पाठवा गुप्त की साइन आउट - म्हणून साइन इन केले + %1$s म्हणून साइन इन केले वगळा वेगळी पद्धत वापरा सत्यापन कोड @@ -176,4 +176,17 @@ मल्टी-फॅक्टर ऑथेंटिकेशन सध्या अक्षम आहे + ते ईमेल किंवा पासवर्ड बरोबर नाही + ते पडताळणी सत्र यापुढे वैध नाही. नवीन कोडची विनंती करा. + फोनची पडताळणी पूर्ण झाली नाही. कृपया पुन्हा प्रयत्न करा. + ती क्रेडेन्शियल वेगळ्या खात्याची आहेत. + तो फोन नंबर या खात्यावर पडताळणीसाठी सेट केलेला नाही. + तुमचे साइन इन सत्र एक्स्पायर झाले आहे. सुरू ठेवण्यासाठी पुन्हा साइन इन करा. + ती लिंक यापुढे वैध नाही. नवीन लिंकची विनंती करा. + तुम्ही पुढे सुरू ठेवण्यापूर्वी तुमच्या ईमेल ॲड्रेसची पडताळणी करा. + ती पडताळणी पद्धत या खात्यावर आधीपासून सेट केलेली आहे. + तुम्ही या खात्यावरील पडताळणी पद्धतींची मर्यादा गाठली आहे. + तुमचा पासवर्ड आवश्यकता पूर्ण करत नाही. दुसरा पासवर्ड वापरून पहा. + पासवर्ड खूप लांब आहे. कमाल लांबी %1$d आहे. + या खात्यासाठी पासकी सापडली नाही. दुसऱ्या पद्धतीने साइन इन करा. diff --git a/auth/src/main/res/values-ms/strings.xml b/auth/src/main/res/values-ms/strings.xml index f06075d773..aa5e91762f 100755 --- a/auth/src/main/res/values-ms/strings.xml +++ b/auth/src/main/res/values-ms/strings.xml @@ -100,15 +100,15 @@ Nombor telefon disahkan secara automatik Hantar Semula Kod Sahkan Nombor Telefon - Use a different phone number + Gunakan nombor telefon lain Dengan mengetik “%1$s”, SMS akan dihantar. Tertakluk pada kadar mesej & data. Dengan mengetik “%1$s”, anda menyatakan bahawa anda menerima %2$s dan %3$s kami. SMS akan dihantar. Tertakluk pada kadar mesej & data. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Ralat Pengesahan + Cuba lagi + Pengesahan tambahan diperlukan. Sila lengkapkan pengesahan berbilang faktor. + Akaun perlu dipautkan. Sila cuba kaedah log masuk yang lain. + Pengesahan dibatalkan. Sila cuba lagi apabila anda bersedia. Pilih Kaedah Pengesahan @@ -166,7 +166,7 @@ Hantar semula e-mel pengesahan Kunci rahsia Log keluar - Log masuk sebagai + Log masuk sebagai %1$s Langkau Gunakan kaedah lain Kod pengesahan @@ -176,4 +176,17 @@ Pengesahan berbilang faktor dilumpuhkan buat masa ini + E-mel atau kata laluan tersebut tidak betul + Sesi pengesahan tersebut tidak sah lagi. Minta kod baharu. + Pengesahan telefon tidak selesai. Sila cuba lagi. + Kelayakan tersebut milik akaun lain. + Nombor telefon tersebut tidak disediakan untuk pengesahan pada akaun ini. + Sesi log masuk anda telah tamat tempoh. Log masuk semula untuk meneruskan. + Pautan tersebut tidak sah lagi. Minta pautan baharu. + Sahkan alamat e-mel anda sebelum anda meneruskan. + Kaedah pengesahan tersebut telah pun disediakan pada akaun ini. + Anda telah mencapai had kaedah pengesahan pada akaun ini. + Kata laluan anda tidak memenuhi keperluan. Cuba kata laluan lain. + Kata laluan terlalu panjang. Panjang maksimum ialah %1$d. + Kami tidak menemui kunci laluan untuk akaun ini. Log masuk dengan cara lain. diff --git a/auth/src/main/res/values-nb/strings.xml b/auth/src/main/res/values-nb/strings.xml index ac494d9cd6..f422080c02 100755 --- a/auth/src/main/res/values-nb/strings.xml +++ b/auth/src/main/res/values-nb/strings.xml @@ -100,7 +100,7 @@ Telefonnummeret ble bekreftet automatisk Send koden på nytt Bekreft telefonnummeret - Use a different phone number + Bruk et annet telefonnummer Når du trykker på «%1$s», kan det bli sendt en SMS. Kostnader for meldinger og datatrafikk kan påløpe. Ved å trykke på «%1$s» godtar du %2$s og %3$s våre. Du kan bli tilsendt en SMS. Kostnader for meldinger og datatrafikk kan påløpe. Godkjenningsfeil @@ -165,7 +165,7 @@ Send bekreftelsese-post på nytt Hemmelig nøkkel Logg ut - Logget inn som + Logget inn som %1$s Hopp over Bruk en annen metode Bekreftelseskode @@ -175,4 +175,17 @@ Flerfaktorautentisering er for øyeblikket deaktivert + E-postadressen eller passordet er feil + Denne bekreftelsesøkten er ikke lenger gyldig. Be om en ny kode. + Telefonbekreftelsen ble ikke fullført. Prøv igjen. + Denne påloggingsinformasjonen tilhører en annen konto. + Dette telefonnummeret er ikke satt opp for bekreftelse på denne kontoen. + Påloggingsøkten din er utløpt. Logg på igjen for å fortsette. + Denne lenken er ikke lenger gyldig. Be om en ny. + Bekreft e-postadressen din før du fortsetter. + Denne bekreftelsesmetoden er allerede satt opp på denne kontoen. + Du har nådd grensen for bekreftelsesmetoder på denne kontoen. + Passordet ditt oppfyller ikke kravene. Prøv et annet. + Passordet er for langt. Maksimal lengde er %1$d. + Vi fant ingen passnøkkel for denne kontoen. Logg på en annen måte. diff --git a/auth/src/main/res/values-nl/strings.xml b/auth/src/main/res/values-nl/strings.xml index 064497191c..62d97f1ccb 100755 --- a/auth/src/main/res/values-nl/strings.xml +++ b/auth/src/main/res/values-nl/strings.xml @@ -100,7 +100,7 @@ Telefoonnummer is automatisch geverifieerd Code opnieuw verzenden Telefoonnummer verifiëren - Use a different phone number + Gebruik een ander telefoonnummer Als u op “%1$s” tikt, ontvangt u mogelijk een sms. Er kunnen sms- en datakosten in rekening worden gebracht. Als u op "%1$s" tikt, geeft u aan dat u onze %2$s en ons %3$s accepteert. Mogelijk ontvangt u een sms. Er kunnen sms- en datakosten in rekening worden gebracht. Authenticatiefout @@ -165,7 +165,7 @@ Verificatie-e-mail opnieuw verzenden Geheime sleutel Uitloggen - Ingelogd als + Ingelogd als %1$s Overslaan Gebruik een andere methode Verificatiecode @@ -175,4 +175,17 @@ Multi-factorauthenticatie is momenteel uitgeschakeld + Dat e-mailadres of wachtwoord is onjuist + Deze verificatiesessie is niet meer geldig. Vraag een nieuwe code aan. + De telefoonverificatie is niet voltooid. Probeer het opnieuw. + Deze inloggegevens horen bij een ander account. + Dat telefoonnummer is niet ingesteld voor verificatie op dit account. + Uw inlogsessie is vervallen. Log opnieuw in om door te gaan. + Deze link is niet meer geldig. Vraag een nieuwe aan. + Bevestig uw e-mailadres voordat u doorgaat. + Deze verificatiemethode is al ingesteld op dit account. + U heeft de limiet voor verificatiemethoden op dit account bereikt. + Uw wachtwoord voldoet niet aan de vereisten. Probeer een ander wachtwoord. + Het wachtwoord is te lang. De maximale lengte is %1$d. + We hebben geen toegangssleutel voor dit account gevonden. Log op een andere manier in. diff --git a/auth/src/main/res/values-no/strings.xml b/auth/src/main/res/values-no/strings.xml index 7df53a222c..9af534059a 100755 --- a/auth/src/main/res/values-no/strings.xml +++ b/auth/src/main/res/values-no/strings.xml @@ -100,15 +100,15 @@ Telefonnummeret ble bekreftet automatisk Send koden på nytt Bekreft telefonnummeret - Use a different phone number + Bruk et annet telefonnummer Når du trykker på «%1$s», kan det bli sendt en SMS. Kostnader for meldinger og datatrafikk kan påløpe. Ved å trykke på «%1$s» godtar du %2$s og %3$s våre. Du kan bli tilsendt en SMS. Kostnader for meldinger og datatrafikk kan påløpe. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Godkjenningsfeil + Prøv igjen + Ytterligere verifisering kreves. Vennligst fullfør multifaktorgodkjenning. + Kontoen må kobles. Prøv en annen påloggingsmetode. + Godkjenning ble avbrutt. Prøv igjen når du er klar. Velg autentiseringsmetode @@ -166,7 +166,7 @@ Send bekreftelsese-post på nytt Hemmelig nøkkel Logg ut - Logget inn som + Logget inn som %1$s Hopp over Bruk en annen metode Bekreftelseskode @@ -176,4 +176,17 @@ Flerfaktorautentisering er for øyeblikket deaktivert + E-postadressen eller passordet er feil + Denne bekreftelsesøkten er ikke lenger gyldig. Be om en ny kode. + Telefonbekreftelsen ble ikke fullført. Prøv igjen. + Denne påloggingsinformasjonen tilhører en annen konto. + Dette telefonnummeret er ikke satt opp for bekreftelse på denne kontoen. + Påloggingsøkten din er utløpt. Logg på igjen for å fortsette. + Denne lenken er ikke lenger gyldig. Be om en ny. + Bekreft e-postadressen din før du fortsetter. + Denne bekreftelsesmetoden er allerede satt opp på denne kontoen. + Du har nådd grensen for bekreftelsesmetoder på denne kontoen. + Passordet ditt oppfyller ikke kravene. Prøv et annet. + Passordet er for langt. Maksimal lengde er %1$d. + Vi fant ingen passnøkkel for denne kontoen. Logg på en annen måte. diff --git a/auth/src/main/res/values-pl/strings.xml b/auth/src/main/res/values-pl/strings.xml index f651ddb899..80940e0f5b 100755 --- a/auth/src/main/res/values-pl/strings.xml +++ b/auth/src/main/res/values-pl/strings.xml @@ -100,7 +100,7 @@ Numer telefonu został automatycznie zweryfikowany Wyślij kod ponownie Zweryfikuj numer telefonu - Use a different phone number + Użyj innego numeru telefonu Gdy klikniesz „%1$s”, może zostać wysłany SMS. Może to skutkować pobraniem opłaty za przesłanie wiadomości i danych. Klikając „%1$s", potwierdzasz, że akceptujesz te dokumenty: %2$s i %3$s. Może zostać wysłany SMS. Może to skutkować pobraniem opłat za przesłanie wiadomości i danych. Błąd uwierzytelniania @@ -165,7 +165,7 @@ Wyślij ponownie e-mail weryfikacyjny Tajny klucz Wyloguj się - Zalogowano jako + Zalogowano jako %1$s Pomiń Użyj innej metody Kod weryfikacyjny @@ -175,4 +175,17 @@ Uwierzytelnianie wieloskładnikowe jest obecnie wyłączone + Ten adres e-mail lub hasło są nieprawidłowe + Ta sesja weryfikacji nie jest już ważna. Poproś o nowy kod. + Weryfikacja numeru telefonu nie została ukończona. Spróbuj ponownie. + Te dane logowania należą do innego konta. + Ten numer telefonu nie jest skonfigurowany do weryfikacji na tym koncie. + Sesja logowania wygasła. Zaloguj się ponownie, aby kontynuować. + Ten link nie jest już ważny. Poproś o nowy. + Zweryfikuj swój adres e-mail, zanim przejdziesz dalej. + Ta metoda weryfikacji jest już skonfigurowana na tym koncie. + Osiągnięto limit metod weryfikacji na tym koncie. + Twoje hasło nie spełnia wymagań. Spróbuj innego. + Hasło jest za długie. Maksymalna długość to %1$d. + Nie znaleziono klucza dostępu do tego konta. Zaloguj się w inny sposób. diff --git a/auth/src/main/res/values-pt-rBR/strings.xml b/auth/src/main/res/values-pt-rBR/strings.xml index 76704c93f6..29c7fe0263 100755 --- a/auth/src/main/res/values-pt-rBR/strings.xml +++ b/auth/src/main/res/values-pt-rBR/strings.xml @@ -47,7 +47,7 @@ Nome e sobrenome Salvar Concluindo sua inscrição… - A senha não é forte o suficiente. Use pelo menos %1$d caractere e combine letras e números. A senha não é forte o suficiente. Use pelo menos %1$d caracteres e combine letras e números. + A senha não é forte o suficiente. Use pelo menos %1$d caractere e combine letras e números. A senha não é forte o suficiente. Use pelo menos %1$d de caracteres e combine letras e números. A senha não é forte o suficiente. Use pelo menos %1$d caracteres e combine letras e números. Falha no registro da conta do e-mail Termos de Serviço Política de privacidade @@ -100,15 +100,15 @@ O número de telefone foi verificado automaticamente Reenviar código Confirmar número de telefone - Use a different phone number + Usar outro número de telefone Se você tocar em “%1$s”, um SMS poderá ser enviado e tarifas de mensagens e de dados serão cobradas. Ao tocar em “%1$s”, você concorda com nossos %2$s e a %3$s. Um SMS poderá ser enviado e tarifas de mensagens e de dados poderão ser cobradas. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Erro de autenticação + Tentar novamente + Verificação adicional necessária. Conclua a autenticação de vários fatores. + A conta precisa ser vinculada. Tente um método de login diferente. + A autenticação foi cancelada. Tente novamente quando estiver pronto. Escolher método de autenticação @@ -194,4 +194,17 @@ A autenticação multifator está atualmente desativada + O e-mail ou a senha está incorreto + Esta sessão de verificação não é mais válida. Solicite um novo código. + A verificação por telefone não foi concluída. Tente novamente. + Essas credenciais pertencem a outra conta. + Esse número de telefone não está configurado para verificação nesta conta. + Sua sessão de login expirou. Faça login novamente para continuar. + Esse link não é mais válido. Solicite um novo. + Verifique seu endereço de e-mail antes de continuar. + Esse método de verificação já está configurado nesta conta. + Você atingiu o limite de métodos de verificação nesta conta. + Sua senha não atende aos requisitos. Tente outra. + A senha é muito longa. O tamanho máximo é %1$d. + Não encontramos uma chave de acesso para esta conta. Faça login de outra forma. diff --git a/auth/src/main/res/values-pt-rPT/strings.xml b/auth/src/main/res/values-pt-rPT/strings.xml index 8c7a2a173d..6296fb7da7 100755 --- a/auth/src/main/res/values-pt-rPT/strings.xml +++ b/auth/src/main/res/values-pt-rPT/strings.xml @@ -47,7 +47,7 @@ Nome próprio e apelido Guardar A realizar a inscrição… - A palavra-passe não é suficientemente forte. Utilize, pelo menos, %1$d caráter e uma combinação de letras e números A palavra-passe não é suficientemente forte. Utilize, pelo menos, %1$d caracteres e uma combinação de letras e números + A palavra-passe não é suficientemente forte. Utilize, pelo menos, %1$d caráter e uma combinação de letras e números A palavra-passe não é suficientemente forte. Utilize, pelo menos, %1$d de caracteres e uma combinação de letras e números A palavra-passe não é suficientemente forte. Utilize, pelo menos, %1$d caracteres e uma combinação de letras e números O registo da conta de email não foi bem-sucedido Termos de Utilização Política de Privacidade @@ -100,15 +100,15 @@ Número de telefone verificado automaticamente Reenviar código Validar número de telefone - Use a different phone number + Usar outro número de telefone Ao tocar em “%1$s”, pode gerar o envio de uma SMS. Podem aplicar-se tarifas de mensagens e dados. Ao tocar em “%1$s”, indica que aceita os %2$s e a %3$s. Pode gerar o envio de uma SMS. Podem aplicar-se tarifas de dados e de mensagens. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Erro de autenticação + Tentar novamente + Verificação adicional necessária. Conclua a autenticação multifator. + A conta tem de ser associada. Experimente um método de início de sessão diferente. + A autenticação foi cancelada. Tente novamente quando estiver pronto. Escolher método de autenticação @@ -194,4 +194,17 @@ A autenticação multifator está atualmente desativada + O email ou a palavra-passe estão incorretos + Esta sessão de validação já não é válida. Solicite um novo código. + A validação do número de telefone não foi concluída. Tente novamente. + Estas credenciais pertencem a outra conta. + Este número de telefone não está configurado para validação nesta conta. + A sua sessão terminou. Inicie sessão novamente para continuar. + Este link já não é válido. Solicite um novo. + Valide o seu endereço de email antes de continuar. + Este método de validação já está configurado nesta conta. + Atingiu o limite de métodos de validação nesta conta. + A sua palavra-passe não cumpre os requisitos. Experimente outra. + A palavra-passe é demasiado longa. O comprimento máximo é %1$d. + Não encontrámos uma chave de acesso para esta conta. Inicie sessão de outra forma. diff --git a/auth/src/main/res/values-pt/strings.xml b/auth/src/main/res/values-pt/strings.xml index 5ee4a9aa8f..35560d1751 100755 --- a/auth/src/main/res/values-pt/strings.xml +++ b/auth/src/main/res/values-pt/strings.xml @@ -47,7 +47,7 @@ Nome e sobrenome Salvar Concluindo sua inscrição… - A senha não é forte o suficiente. Use pelo menos %1$d caractere e combine letras e números. A senha não é forte o suficiente. Use pelo menos %1$d caracteres e combine letras e números. + A senha não é forte o suficiente. Use pelo menos %1$d caractere e combine letras e números. A senha não é forte o suficiente. Use pelo menos %1$d de caracteres e combine letras e números. A senha não é forte o suficiente. Use pelo menos %1$d caracteres e combine letras e números. Falha no registro da conta do e-mail Termos de Serviço Política de privacidade @@ -100,7 +100,7 @@ O número de telefone foi verificado automaticamente Reenviar código Confirmar número de telefone - Use a different phone number + Usar outro número de telefone Se você tocar em “%1$s”, um SMS poderá ser enviado e tarifas de mensagens e de dados serão cobradas. Ao tocar em "%1$s", você concorda com nossos %2$s e a %3$s. Um SMS poderá ser enviado e tarifas de mensagens e de dados poderão ser cobradas. Erro de autenticação @@ -193,4 +193,17 @@ A autenticação multifator está atualmente desativada + O e-mail ou a senha está incorreto + Esta sessão de verificação não é mais válida. Solicite um novo código. + A verificação por telefone não foi concluída. Tente novamente. + Essas credenciais pertencem a outra conta. + Esse número de telefone não está configurado para verificação nesta conta. + Sua sessão de login expirou. Faça login novamente para continuar. + Esse link não é mais válido. Solicite um novo. + Verifique seu endereço de e-mail antes de continuar. + Esse método de verificação já está configurado nesta conta. + Você atingiu o limite de métodos de verificação nesta conta. + Sua senha não atende aos requisitos. Tente outra. + A senha é muito longa. O tamanho máximo é %1$d. + Não encontramos uma chave de acesso para esta conta. Faça login de outra forma. diff --git a/auth/src/main/res/values-ro/strings.xml b/auth/src/main/res/values-ro/strings.xml index 6b0d3a0486..1d30abbf02 100755 --- a/auth/src/main/res/values-ro/strings.xml +++ b/auth/src/main/res/values-ro/strings.xml @@ -100,7 +100,7 @@ Numărul de telefon este verificat automat Retrimiteți codul Confirmați numărul de telefon - Use a different phone number + Folosiți alt număr de telefon Dacă atingeți „%1$s”, poate fi trimis un SMS. Se pot aplica tarife pentru mesaje și date. Dacă atingeți „%1$s", sunteți de acord cu %2$s și cu %3$s. Poate fi trimis un SMS. Se pot aplica tarife pentru mesaje și date. Eroare de autentificare @@ -165,7 +165,7 @@ Retrimite e-mailul de verificare Cheie secretă Deconectare - Conectat ca + Conectat ca %1$s Omite Folosiți o altă metodă Cod de verificare @@ -175,4 +175,17 @@ Autentificarea cu mai mulți factori este dezactivată în prezent + Adresa de e-mail sau parola nu este corectă + Această sesiune de verificare nu mai este validă. Solicitați un cod nou. + Verificarea numărului de telefon nu a fost finalizată. Încercați din nou. + Aceste date de conectare aparțin altui cont. + Acest număr de telefon nu este configurat pentru verificare în acest cont. + Sesiunea de conectare a expirat. Conectați-vă din nou pentru a continua. + Acest link nu mai este valid. Solicitați unul nou. + Confirmați adresa de e-mail înainte de a continua. + Această metodă de verificare este deja configurată în acest cont. + Ați atins limita de metode de verificare pentru acest cont. + Parola nu îndeplinește cerințele. Încercați alta. + Parola este prea lungă. Lungimea maximă este %1$d. + Nu am găsit o cheie de acces pentru acest cont. Conectați-vă în alt mod. diff --git a/auth/src/main/res/values-ru/strings.xml b/auth/src/main/res/values-ru/strings.xml index ebf567549e..8fbc42a50a 100755 --- a/auth/src/main/res/values-ru/strings.xml +++ b/auth/src/main/res/values-ru/strings.xml @@ -100,7 +100,7 @@ Номер телефона был подтвержден автоматически Отправить код ещё раз Подтвердить номер телефона - Use a different phone number + Использовать другой номер телефона Нажимая кнопку “%1$s”, вы соглашаетесь получить SMS. За его отправку и обмен данными может взиматься плата. Нажимая кнопку "%1$s", вы принимаете %2$s и %3$s, а также соглашаетесь получить SMS. За его отправку и обмен данными может взиматься плата. Ошибка аутентификации @@ -165,7 +165,7 @@ Отправить письмо подтверждения повторно Секретный ключ Выйти - Вы вошли как + Вы вошли как %1$s Пропустить Использовать другой способ Код подтверждения @@ -175,4 +175,17 @@ Многофакторная аутентификация в настоящее время отключена + Неправильный адрес электронной почты или пароль + Этот сеанс проверки больше не действителен. Запросите новый код. + Не удалось подтвердить номер телефона. Повторите попытку. + Эти учётные данные принадлежат другому аккаунту. + Этот номер телефона не настроен для подтверждения в этом аккаунте. + Сеанс входа истёк. Войдите ещё раз, чтобы продолжить. + Эта ссылка больше не действительна. Запросите новую. + Подтвердите адрес электронной почты, прежде чем продолжить. + Этот способ подтверждения уже настроен в этом аккаунте. + Достигнут лимит способов подтверждения для этого аккаунта. + Пароль не соответствует требованиям. Попробуйте другой. + Пароль слишком длинный. Максимальная длина – %1$d. + Не удалось найти ключ доступа для этого аккаунта. Войдите другим способом. diff --git a/auth/src/main/res/values-sk/strings.xml b/auth/src/main/res/values-sk/strings.xml index 0786594fd9..c85de5c935 100755 --- a/auth/src/main/res/values-sk/strings.xml +++ b/auth/src/main/res/values-sk/strings.xml @@ -100,7 +100,7 @@ Telefónne číslo bolo automaticky overené Znova odoslať kód Overiť telefónne číslo - Use a different phone number + Použiť iné telefónne číslo Klepnutím na tlačidlo %1$s možno odoslať SMS. Môžu sa účtovať poplatky za správy a dáta. Klepnutím na tlačidlo %1$s vyjadrujete súhlas s dokumentmi %2$s a %3$s. Môže byť odoslaná SMS a môžu sa účtovať poplatky za správy a dáta. Chyba overenia @@ -165,7 +165,7 @@ Znova poslať overovací e-mail Tajný kľúč Odhlásiť sa - Prihlásený ako + Prihlásený ako %1$s Preskočiť Použiť inú metódu Overovací kód @@ -175,4 +175,17 @@ Viacfaktorové overovanie je momentálne zakázané + Tento e-mail alebo heslo nie je správne + Táto overovacia relácia už nie je platná. Vyžiadajte si nový kód. + Overenie telefónneho čísla sa nedokončilo. Skúste to znova. + Tieto prihlasovacie údaje patria inému účtu. + Toto telefónne číslo nie je v tomto účte nastavené na overovanie. + Vaša prihlasovacia relácia vypršala. Ak chcete pokračovať, prihláste sa znova. + Tento odkaz už nie je platný. Vyžiadajte si nový. + Skôr než budete pokračovať, overte svoju e-mailovú adresu. + Táto metóda overenia je v tomto účte už nastavená. + Dosiahli ste limit metód overenia pre tento účet. + Vaše heslo nespĺňa požiadavky. Skúste iné. + Heslo je príliš dlhé. Maximálna dĺžka je %1$d. + Pre tento účet sa nenašiel prístupový kľúč. Prihláste sa iným spôsobom. diff --git a/auth/src/main/res/values-sl/strings.xml b/auth/src/main/res/values-sl/strings.xml index 3375bd8c01..e8f63e5178 100755 --- a/auth/src/main/res/values-sl/strings.xml +++ b/auth/src/main/res/values-sl/strings.xml @@ -100,15 +100,15 @@ Telefonska številka je bila samodejno preverjena Ponovno pošlji kodo Preverjanje telefonske številke - Use a different phone number + Uporabi drugo telefonsko številko Če se dotaknete možnosti »%1$s«, bo morda poslano sporočilo SMS. Pošiljanje sporočila in prenos podatkov boste morda morali plačati. Če se dotaknete možnosti »%1$s«, potrjujete, da se strinjate z dokumentoma %2$s in %3$s. Morda bo poslano sporočilo SMS. Pošiljanje sporočila in prenos podatkov boste morda morali plačati. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Napaka pri preverjanju pristnosti + Poskusite znova + Potrebno je dodatno preverjanje. Dokončajte večstopenjsko preverjanje pristnosti. + Račun je treba povezati. Poskusite z drugim načinom prijave. + Preverjanje pristnosti je bilo preklicano. Ko boste pripravljeni, poskusite znova. Izberite način preverjanja pristnosti @@ -166,7 +166,7 @@ Znova pošlji e-sporočilo za preverjanje Skrivni ključ Odjava - Prijavljen kot + Prijavljen kot %1$s Preskoči Uporabi drugo metodo Koda za preverjanje @@ -176,4 +176,17 @@ Večfaktorska avtentikacija je trenutno onemogočena + E-poštni naslov ali geslo ni pravilno + Ta seja preverjanja ni več veljavna. Zahtevajte novo kodo. + Preverjanje telefonske številke ni bilo dokončano. Poskusite znova. + Ti poverilnici pripadata drugemu računu. + Ta telefonska številka v tem računu ni nastavljena za preverjanje. + Vaša prijavna seja je potekla. Za nadaljevanje se znova prijavite. + Ta povezava ni več veljavna. Zahtevajte novo. + Preden nadaljujete, preverite svoj e-poštni naslov. + Ta način preverjanja je v tem računu že nastavljen. + Dosegli ste omejitev števila načinov preverjanja v tem računu. + Vaše geslo ne izpolnjuje zahtev. Poskusite z drugim. + Geslo je predolgo. Največja dolžina je %1$d. + Za ta račun nismo našli ključa za dostop. Prijavite se na drug način. diff --git a/auth/src/main/res/values-sr/strings.xml b/auth/src/main/res/values-sr/strings.xml index 7266b80e4f..fba17a18ec 100755 --- a/auth/src/main/res/values-sr/strings.xml +++ b/auth/src/main/res/values-sr/strings.xml @@ -100,15 +100,15 @@ Број телефона је аутоматски верификован Поново пошаљи кôд Верификуј број телефона - Use a different phone number + Користи други број телефона Ако додирнете „%1$s“, можда ћете послати SMS. Могу да вам буду наплаћени трошкови слања поруке и преноса података. Ако додирнете „%1$s“, потврђујете да прихватате документе %2$s и %3$s. Можда ћете послати SMS. Могу да вам буду наплаћени трошкови слања поруке и преноса података. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Грешка при аутентификацији + Пробајте поново + Потребна је додатна верификација. Довршите аутентификацију са више фактора. + Налог треба да се повеже. Пробајте други метод пријављивања. + Аутентификација је отказана. Пробајте поново када будете спремни. Изаберите метод аутентификације @@ -166,7 +166,7 @@ Поново пошаљи имејл за верификацију Тајни кључ Одјава - Пријављен као + Пријављен као %1$s Прескочи Користи други метод Код за верификацију @@ -176,4 +176,17 @@ Вишефакторска аутентификација је тренутно онемогућена + Та имејл адреса или лозинка није тачна + Та сесија верификације више не важи. Затражите нови кôд. + Верификација броја телефона није довршена. Пробајте поново. + Ти акредитиви припадају другом налогу. + Тај број телефона није подешен за верификацију на овом налогу. + Сесија пријављивања је истекла. Пријавите се поново да бисте наставили. + Та веза више не важи. Затражите нову. + Верификујте имејл адресу пре него што наставите. + Тај метод верификације је већ подешен на овом налогу. + Достигли сте ограничење броја метода верификације на овом налогу. + Лозинка не испуњава услове. Пробајте другу. + Лозинка је предугачка. Максимална дужина је %1$d. + Нисмо пронашли приступни кључ за овај налог. Пријавите се на други начин. diff --git a/auth/src/main/res/values-sv/strings.xml b/auth/src/main/res/values-sv/strings.xml index 2964dbcc79..4a95f80b3e 100755 --- a/auth/src/main/res/values-sv/strings.xml +++ b/auth/src/main/res/values-sv/strings.xml @@ -100,7 +100,7 @@ Telefonnumret verifierades automatiskt Skicka koden igen Verifiera telefonnummer - Use a different phone number + Använd ett annat telefonnummer Genom att trycka på %1$s skickas ett sms. Meddelande- och dataavgifter kan tillkomma. Genom att trycka på %1$s godkänner du våra %2$s och vår %3$s. Ett sms kan skickas. Meddelande- och dataavgifter kan tillkomma. Autentiseringsfel @@ -165,7 +165,7 @@ Skicka verifieringsmail igen Hemlig nyckel Logga ut - Inloggad som + Inloggad som %1$s Hoppa över Använd en annan metod Verifieringskod @@ -175,4 +175,17 @@ Multifaktorautentisering är för närvarande inaktiverad + E-postadressen eller lösenordet är felaktigt + Verifieringssessionen är inte längre giltig. Begär en ny kod. + Telefonverifieringen slutfördes inte. Försök igen. + De här inloggningsuppgifterna tillhör ett annat konto. + Det telefonnumret är inte konfigurerat för verifiering på det här kontot. + Din inloggningssession har upphört att gälla. Logga in igen för att fortsätta. + Länken är inte längre giltig. Begär en ny. + Verifiera din e-postadress innan du fortsätter. + Den verifieringsmetoden är redan konfigurerad på det här kontot. + Du har nått gränsen för antalet verifieringsmetoder på det här kontot. + Ditt lösenord uppfyller inte kraven. Prova ett annat. + Lösenordet är för långt. Den maximala längden är %1$d. + Vi hittade ingen nyckel för det här kontot. Logga in på ett annat sätt. diff --git a/auth/src/main/res/values-ta/strings.xml b/auth/src/main/res/values-ta/strings.xml index f268d402b8..851920ae79 100755 --- a/auth/src/main/res/values-ta/strings.xml +++ b/auth/src/main/res/values-ta/strings.xml @@ -100,15 +100,15 @@ ஃபோன் எண் தானாகவே சரிபார்க்கப்பட்டது குறியீட்டை மீண்டும் அனுப்பு ஃபோன் எண்ணைச் சரிபார் - Use a different phone number + வேறு ஃபோன் எண்ணைப் பயன்படுத்து “%1$s” என்பதைத் தட்டுவதன் மூலம், SMS அனுப்பப்படலாம். செய்தி மற்றும் தரவுக் கட்டணங்கள் விதிக்கப்படலாம். “%1$s” என்பதைத் தட்டுவதன் மூலம், எங்கள் %2$s மற்றும் %3$sஐ ஏற்பதாகக் குறிப்பிடுகிறீர்கள். SMS அனுப்பப்படலாம். செய்தி மற்றும் தரவுக் கட்டணங்கள் விதிக்கப்படலாம். - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + அங்கீகாரப் பிழை + மீண்டும் முயலவும் + கூடுதல் சரிபார்ப்பு தேவை. பல்காரணி அங்கீகாரத்தை நிறைவுசெய்யவும். + கணக்கை இணைக்க வேண்டும். வேறு உள்நுழைவு முறையை முயலவும். + அங்கீகாரம் ரத்துசெய்யப்பட்டது. தயாரானதும் மீண்டும் முயலவும். அங்கீகார முறையைத் தேர்ந்தெடுக்கவும் @@ -166,7 +166,7 @@ சரிபார்ப்பு மின்னஞ்சலை மீண்டும் அனுப்பு ரகசிய திறவுகோல் வெளியேறு - இவராக உள்நுழைந்துள்ளது + %1$sஆக உள்நுழைந்துள்ளது தவிர் வேறு முறையைப் பயன்படுத்து சரிபார்ப்பு குறியீடு @@ -176,4 +176,17 @@ பல-காரணி அங்கீகாரம் தற்போது முடக்கப்பட்டுள்ளது + அந்த மின்னஞ்சல் முகவரியோ கடவுச்சொல்லோ சரியில்லை + அந்தச் சரிபார்ப்பு அமர்வு இனி செல்லுபடியாகாது. புதிய குறியீட்டைக் கோரவும். + ஃபோன் சரிபார்ப்பு நிறைவடையவில்லை. மீண்டும் முயலவும். + அந்த அனுமதிச் சான்றுகள் வேறொரு கணக்கிற்கு உரியவை. + அந்த ஃபோன் எண் இந்தக் கணக்கில் சரிபார்ப்பிற்காக அமைக்கப்படவில்லை. + உங்கள் உள்நுழைவு அமர்வு காலாவதியானது. தொடர, மீண்டும் உள்நுழையவும். + அந்த இணைப்பு இனி செல்லுபடியாகாது. புதியதொன்றைக் கோரவும். + தொடர்வதற்கு முன் உங்கள் மின்னஞ்சல் முகவரியைச் சரிபார்க்கவும். + அந்தச் சரிபார்ப்பு முறை இந்தக் கணக்கில் ஏற்கெனவே அமைக்கப்பட்டுள்ளது. + இந்தக் கணக்கிற்கான சரிபார்ப்பு முறைகளின் வரம்பை எட்டிவிட்டீர்கள். + உங்கள் கடவுச்சொல் தேவைகளைப் பூர்த்தி செய்யவில்லை. வேறொன்றை முயலவும். + கடவுச்சொல் மிக நீளமாக உள்ளது. அதிகபட்ச நீளம் %1$d ஆகும். + இந்தக் கணக்கிற்கான கடவுச்சாவி கிடைக்கவில்லை. வேறு முறையில் உள்நுழையவும். diff --git a/auth/src/main/res/values-th/strings.xml b/auth/src/main/res/values-th/strings.xml index ad9f71a6b4..14461d729d 100755 --- a/auth/src/main/res/values-th/strings.xml +++ b/auth/src/main/res/values-th/strings.xml @@ -100,15 +100,15 @@ ยืนยันหมายเลขโทรศัพท์โดยอัตโนมัติแล้ว ส่งรหัสอีกครั้ง ยืนยันหมายเลขโทรศัพท์ - Use a different phone number + ใช้หมายเลขโทรศัพท์อื่น เมื่อคุณแตะ “%1$s” ระบบจะส่ง SMS ให้คุณ อาจมีค่าบริการรับส่งข้อความและค่าบริการอินเทอร์เน็ต การแตะ “%1$s” แสดงว่าคุณยอมรับ %2$s และ %3$s ระบบจะส่ง SMS ให้คุณ อาจมีค่าบริการรับส่งข้อความและค่าบริการอินเทอร์เน็ต - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + ข้อผิดพลาดในการตรวจสอบสิทธิ์ + ลองอีกครั้ง + ต้องมีการยืนยันเพิ่มเติม โปรดดำเนินการยืนยันตัวตนแบบหลายปัจจัยให้เสร็จสมบูรณ์ + ต้องลิงก์บัญชี โปรดลองใช้วิธีลงชื่อเข้าใช้วิธีอื่น + การตรวจสอบสิทธิ์ถูกยกเลิก โปรดลองอีกครั้งเมื่อคุณพร้อม เลือกวิธีการตรวจสอบสิทธิ์ @@ -166,7 +166,7 @@ ส่งอีเมลยืนยันอีกครั้ง คีย์ลับ ออกจากระบบ - ลงชื่อเข้าใช้ในฐานะ + ลงชื่อเข้าใช้ในฐานะ %1$s ข้าม ใช้วิธีอื่น รหัสยืนยัน @@ -176,4 +176,17 @@ การรับรองความถูกต้องแบบหลายปัจจัยถูกปิดใช้งานในขณะนี้ + อีเมลหรือรหัสผ่านไม่ถูกต้อง + เซสชันการยืนยันนี้ใช้ไม่ได้อีกต่อไป โปรดขอรหัสใหม่ + การยืนยันหมายเลขโทรศัพท์ไม่เสร็จสมบูรณ์ โปรดลองอีกครั้ง + ข้อมูลเข้าสู่ระบบนี้เป็นของบัญชีอื่น + หมายเลขโทรศัพท์นี้ไม่ได้ตั้งค่าไว้สำหรับการยืนยันในบัญชีนี้ + เซสชันการลงชื่อเข้าใช้หมดอายุแล้ว โปรดลงชื่อเข้าใช้อีกครั้งเพื่อดำเนินการต่อ + ลิงก์นี้ใช้ไม่ได้อีกต่อไป โปรดขอลิงก์ใหม่ + โปรดยืนยันที่อยู่อีเมลก่อนดำเนินการต่อ + วิธีการยืนยันนี้ตั้งค่าไว้ในบัญชีนี้แล้ว + คุณใช้วิธีการยืนยันถึงขีดจำกัดของบัญชีนี้แล้ว + รหัสผ่านของคุณไม่เป็นไปตามข้อกำหนด โปรดลองใช้รหัสผ่านอื่น + รหัสผ่านยาวเกินไป ความยาวสูงสุดคือ %1$d + ไม่พบพาสคีย์สำหรับบัญชีนี้ โปรดลงชื่อเข้าใช้ด้วยวิธีอื่น diff --git a/auth/src/main/res/values-tl/strings.xml b/auth/src/main/res/values-tl/strings.xml index 90e55a4704..7b6bd865a9 100755 --- a/auth/src/main/res/values-tl/strings.xml +++ b/auth/src/main/res/values-tl/strings.xml @@ -100,7 +100,7 @@ Awtomatikong na-verify ang numero ng telepono Ipadala Muli ang Code I-verify ang Numero ng Telepono - Use a different phone number + Gumamit ng ibang numero ng telepono Sa pag-tap sa “%1$s,“ maaaring magpadala ng SMS. Maaaring ipatupad ang mga rate ng pagmemensahe at data. Sa pag-tap sa “%1$s”, ipinababatid mo na tinatanggap mo ang aming %2$s at %3$s. Maaaring magpadala ng SMS. Maaaring ipatupad ang mga rate ng pagmemensahe at data. Error sa Pagpapatotoo @@ -146,7 +146,7 @@ Pumili ng paraan ng pag-verify Magdagdag ng karagdagang layer ng seguridad SMS - Authenticator app + App ng authenticator Ang numerong ito ay nauugnay sa ibang account Kinakailangan ang pag-verify I-scan ang QR code gamit ang iyong authenticator app @@ -163,16 +163,29 @@ Mag-authenticate muli Alisin Ipadala muli ang verification email - Secret key + Sikretong key Mag-sign out - Naka-sign in bilang + Naka-sign in bilang %1$s Laktawan Gumamit ng ibang paraan - Verification code + Code sa pag-verify Na-verify ang email I-verify Nagpadala kami ng verification email sa %1$s Kasalukuyang naka-disable ang multi-factor authentication + Mali ang email address o password na iyon + Wala nang bisa ang verification session na iyon. Humiling ng bagong code. + Hindi nakumpleto ang pag-verify ng telepono. Subukan muli. + Kabilang ang mga kredensyal na iyon sa ibang account. + Hindi naka-set up ang numero ng teleponong iyon para sa pag-verify sa account na ito. + Nag-expire na ang iyong sign-in session. Mag-sign in muli para magpatuloy. + Wala nang bisa ang link na iyon. Humiling ng bago. + I-verify ang iyong email address bago ka magpatuloy. + Naka-set up na ang paraan ng pag-verify na iyon sa account na ito. + Naabot mo na ang limitasyon ng mga paraan ng pag-verify sa account na ito. + Hindi natutugunan ng iyong password ang mga kinakailangan. Sumubok ng iba. + Masyadong mahaba ang password. Ang maximum na haba ay %1$d. + Wala kaming nakitang passkey para sa account na ito. Mag-sign in sa ibang paraan. diff --git a/auth/src/main/res/values-tr/strings.xml b/auth/src/main/res/values-tr/strings.xml index 15a7d80b91..235595d928 100755 --- a/auth/src/main/res/values-tr/strings.xml +++ b/auth/src/main/res/values-tr/strings.xml @@ -100,15 +100,15 @@ Telefon numarası otomatik olarak doğrulandı Kodu Yeniden Gönder Telefon Numarasını Doğrula - Use a different phone number + Farklı bir telefon numarası kullan “%1$s” öğesine dokunarak SMS gönderilebilir. Mesaj ve veri ücretleri uygulanabilir. “%1$s” öğesine dokunarak %2$s ve %3$s hükümlerimizi kabul ettiğinizi bildirirsiniz. SMS gönderilebilir. Mesaj ve veri ücretleri uygulanabilir. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Kimlik Doğrulama Hatası + Tekrar dene + Ek doğrulama gerekiyor. Lütfen çok faktörlü kimlik doğrulamayı tamamlayın. + Hesabın bağlanması gerekiyor. Lütfen farklı bir oturum açma yöntemi deneyin. + Kimlik doğrulama iptal edildi. Hazır olduğunuzda tekrar deneyin. Kimlik Doğrulama Yöntemini Seçin @@ -166,7 +166,7 @@ Doğrulama e-postasını tekrar gönder Gizli anahtar Çıkış yap - Şu kullanıcı olarak oturum açıldı + %1$s olarak oturum açıldı Atla Farklı bir yöntem kullan Doğrulama kodu @@ -176,4 +176,17 @@ Çok faktörlü kimlik doğrulama şu anda devre dışı + E-posta adresi veya şifre yanlış + Bu doğrulama oturumu artık geçerli değil. Yeni bir kod isteyin. + Telefon doğrulaması tamamlanmadı. Tekrar deneyin. + Bu kimlik bilgileri başka bir hesaba ait. + Bu telefon numarası, bu hesapta doğrulama için ayarlanmamış. + Oturumunuzun süresi doldu. Devam etmek için tekrar oturum açın. + Bu bağlantı artık geçerli değil. Yeni bir tane isteyin. + Devam etmeden önce e-posta adresinizi doğrulayın. + Bu doğrulama yöntemi bu hesapta zaten ayarlanmış. + Bu hesap için doğrulama yöntemi sınırına ulaştınız. + Şifreniz gereksinimleri karşılamıyor. Başka bir şifre deneyin. + Şifre çok uzun. İzin verilen en fazla uzunluk %1$d. + Bu hesap için parola anahtarı bulunamadı. Başka bir yöntemle oturum açın. diff --git a/auth/src/main/res/values-uk/strings.xml b/auth/src/main/res/values-uk/strings.xml index 338339d6c1..3842de224e 100755 --- a/auth/src/main/res/values-uk/strings.xml +++ b/auth/src/main/res/values-uk/strings.xml @@ -100,15 +100,15 @@ Номер телефону підтверджено автоматично Повторно надіслати код Підтвердити номер телефону - Use a different phone number + Використати інший номер телефону Коли ви торкнетесь опції “%1$s”, вам може надійти SMS-повідомлення. За SMS і використання трафіку може стягуватися плата. Торкаючись кнопки “%1$s”, ви приймаєте такі документи: %2$s і %3$s. Вам може надійти SMS-повідомлення. За SMS і використання трафіку може стягуватися плата. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Помилка автентифікації + Повторити спробу + Потрібне додаткове підтвердження. Виконайте багатофакторну автентифікацію. + Обліковий запис потрібно зв\'язати. Спробуйте інший спосіб входу. + Автентифікацію скасовано. Повторіть спробу, коли будете готові. Виберіть спосіб автентифікації @@ -166,7 +166,7 @@ Надіслати лист підтвердження повторно Секретний ключ Вийти - Ви ввійшли як + Ви ввійшли як %1$s Пропустити Використати інший спосіб Код підтвердження @@ -176,4 +176,17 @@ Багатофакторна автентифікація наразі вимкнена + Неправильна електронна адреса або пароль + Цей сеанс підтвердження більше не дійсний. Запросіть новий код. + Не вдалося завершити підтвердження номера телефону. Повторіть спробу. + Ці облікові дані належать іншому обліковому запису. + Цей номер телефону не налаштовано для підтвердження в цьому обліковому записі. + Сеанс входу закінчився. Увійдіть знову, щоб продовжити. + Це посилання більше не дійсне. Запросіть нове. + Підтвердьте свою електронну адресу, перш ніж продовжити. + Цей спосіб підтвердження вже налаштовано в цьому обліковому записі. + Ви досягли ліміту способів підтвердження для цього облікового запису. + Пароль не відповідає вимогам. Спробуйте інший. + Пароль задовгий. Максимальна довжина – %1$d. + Не вдалося знайти ключ доступу для цього облікового запису. Увійдіть іншим способом. diff --git a/auth/src/main/res/values-ur/strings.xml b/auth/src/main/res/values-ur/strings.xml index aa780c0fb3..d9e419c2ec 100755 --- a/auth/src/main/res/values-ur/strings.xml +++ b/auth/src/main/res/values-ur/strings.xml @@ -100,15 +100,15 @@ فون نمبر کی خودکار طور پر توثیق ہو گئی کوڈ دوبارہ بھیجیں فون نمبر کی توثیق کریں - Use a different phone number + مختلف فون نمبر استعمال کریں %1$s پر تھپتھپانے سے، ایک SMS بھیجا جا سکتا ہے۔ پیغام اور ڈیٹا کی شرحوں کا اطلاق ہو سکتا ہے۔ “%1$s” کو تھپتھپا کر، آپ نشاندہی کر رہے ہیں کہ آپ ہماری %2$s اور %3$s کو قبول کرتے ہیں۔ ایک SMS بھیجا جا سکتا ہے۔ پیغام اور ڈیٹا نرخ لاگو ہو سکتے ہیں۔ - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + تصدیق کی خرابی + دوبارہ کوشش کریں + اضافی تصدیق درکار ہے۔ براہ کرم کثیر عنصری تصدیق مکمل کریں۔ + اکاؤنٹ کو لنک کرنے کی ضرورت ہے۔ براہ کرم مختلف سائن ان طریقہ آزمائیں۔ + تصدیق منسوخ کر دی گئی۔ تیار ہونے پر دوبارہ کوشش کریں۔ تصدیقی طریقہ منتخب کریں @@ -166,7 +166,7 @@ تصدیقی ای میل دوبارہ بھیجیں خفیہ کلید سائن آؤٹ - بطور سائن ان ہیں + بطور %1$s سائن ان ہیں چھوڑیں ایک مختلف طریقہ استعمال کریں تصدیقی کوڈ @@ -176,4 +176,17 @@ ملٹی فیکٹر تصدیق فی الحال غیر فعال ہے + وہ ای میل یا پاس ورڈ درست نہیں ہے + یہ توثیقی سیشن اب کارآمد نہیں رہا۔ نیا کوڈ طلب کریں۔ + فون کی توثیق مکمل نہیں ہوئی۔ دوبارہ کوشش کریں۔ + یہ اسناد کسی دوسرے اکاؤنٹ سے تعلق رکھتی ہیں۔ + یہ فون نمبر اس اکاؤنٹ پر توثیق کے لیے سیٹ اپ نہیں ہے۔ + آپ کا سائن ان سیشن ختم ہو گیا۔ جاری رکھنے کے لیے دوبارہ سائن ان کریں۔ + یہ لنک اب کارآمد نہیں رہا۔ نیا لنک طلب کریں۔ + جاری رکھنے سے پہلے اپنے ای میل پتے کی توثیق کریں۔ + یہ توثیقی طریقہ اس اکاؤنٹ پر پہلے سے سیٹ اپ ہے۔ + آپ اس اکاؤنٹ پر توثیقی طریقوں کی حد تک پہنچ گئے ہیں۔ + آپ کا پاس ورڈ تقاضے پورے نہیں کرتا۔ کوئی دوسرا آزمائیں۔ + پاس ورڈ بہت طویل ہے۔ زیادہ سے زیادہ طوالت %1$d ہے۔ + اس اکاؤنٹ کے لیے پاس کی نہیں ملی۔ کسی اور طریقے سے سائن ان کریں۔ diff --git a/auth/src/main/res/values-vi/strings.xml b/auth/src/main/res/values-vi/strings.xml index c2a0b99938..8af3065ab4 100755 --- a/auth/src/main/res/values-vi/strings.xml +++ b/auth/src/main/res/values-vi/strings.xml @@ -10,7 +10,7 @@ Twitter GitHub Điện thoại - Email + Địa chỉ email Đăng nhập bằng Google Đăng nhập bằng Google Đăng nhập bằng Facebook @@ -31,7 +31,7 @@ Đăng nhập bằng Yahoo Đăng nhập bằng Yahoo Tiếp - Email + Địa chỉ email Số điện thoại Quốc gia Chọn quốc gia @@ -100,15 +100,15 @@ Đã tự động xác minh số điện thoại Gửi lại mã Xác minh số điện thoại - Use a different phone number + Dùng số điện thoại khác Bằng cách nhấn vào “%1$s”, bạn có thể nhận được một tin nhắn SMS. Cước tin nhắn và dữ liệu có thể áp dụng. Bằng cách nhấn vào “%1$s”, bạn cho biết rằng bạn chấp nhận %2$s và %3$s của chúng tôi. Bạn có thể nhận được một tin nhắn SMS. Cước tin nhắn và dữ liệu có thể áp dụng. - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + Lỗi xác thực + Thử lại + Bạn cần xác minh thêm. Vui lòng hoàn tất quy trình xác thực nhiều yếu tố. + Bạn cần liên kết tài khoản. Vui lòng thử một phương thức đăng nhập khác. + Quá trình xác thực đã bị hủy. Vui lòng thử lại khi bạn sẵn sàng. Chọn phương thức xác thực @@ -166,7 +166,7 @@ Gửi lại email xác minh Khóa bí mật Đăng xuất - Đã đăng nhập với tư cách + Đã đăng nhập với tư cách %1$s Bỏ qua Sử dụng phương thức khác Mã xác minh @@ -176,4 +176,17 @@ Xác thực đa yếu tố hiện đang bị vô hiệu hóa + Email hoặc mật khẩu không chính xác + Phiên xác minh này không còn hợp lệ. Hãy yêu cầu mã mới. + Quá trình xác minh số điện thoại chưa hoàn tất. Hãy thử lại. + Thông tin đăng nhập này thuộc về một tài khoản khác. + Số điện thoại này chưa được thiết lập để xác minh trên tài khoản này. + Phiên đăng nhập của bạn đã hết hạn. Hãy đăng nhập lại để tiếp tục. + Đường liên kết này không còn hợp lệ. Hãy yêu cầu một đường liên kết mới. + Hãy xác minh địa chỉ email của bạn trước khi tiếp tục. + Phương thức xác minh này đã được thiết lập trên tài khoản này. + Bạn đã đạt đến giới hạn số phương thức xác minh trên tài khoản này. + Mật khẩu của bạn không đáp ứng các yêu cầu. Hãy thử mật khẩu khác. + Mật khẩu quá dài. Độ dài tối đa là %1$d. + Không tìm thấy mã xác thực cho tài khoản này. Hãy đăng nhập bằng cách khác. diff --git a/auth/src/main/res/values-zh-rCN/strings.xml b/auth/src/main/res/values-zh-rCN/strings.xml index 12fb4c3dd8..40bab56b9d 100755 --- a/auth/src/main/res/values-zh-rCN/strings.xml +++ b/auth/src/main/res/values-zh-rCN/strings.xml @@ -100,15 +100,15 @@ 电话号码已自动验证 重新发送验证码 验证电话号码 - Use a different phone number + 使用其他电话号码 您点按“%1$s”后,系统会向您发送一条短信。这可能会产生短信费用和上网流量费。 点按“%1$s”即表示您接受我们的%2$s和%3$s。系统会向您发送一条短信。这可能会产生短信费用和上网流量费。 - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + 身份验证错误 + 重试 + 需要额外的验证。请完成多重身份验证。 + 需要关联账户。请尝试其他登录方式。 + 身份验证已取消。准备好后请重试。 选择身份验证方法 @@ -166,7 +166,7 @@ 重新发送验证邮件 密钥 退出 - 登录身份 + 登录身份:%1$s 跳过 使用其他方式 验证码 @@ -176,4 +176,17 @@ 多重身份验证当前已禁用 + 电子邮件地址或密码不正确 + 该验证会话已失效,请重新获取验证码。 + 电话号码验证未完成,请重试。 + 这些凭据属于其他帐号。 + 该电话号码未在此帐号中设置为验证方式。 + 您的登录会话已过期,请重新登录以继续。 + 该链接已失效,请重新获取。 + 请先验证您的电子邮件地址,然后再继续。 + 该验证方式已在此帐号中设置。 + 您已达到此帐号的验证方式数量上限。 + 您的密码不符合要求,请尝试其他密码。 + 密码过长。最大长度为 %1$d。 + 未找到此账号的通行密钥,请通过其他方式登录。 diff --git a/auth/src/main/res/values-zh-rHK/strings.xml b/auth/src/main/res/values-zh-rHK/strings.xml index fb7a627ae6..972bf2edd4 100755 --- a/auth/src/main/res/values-zh-rHK/strings.xml +++ b/auth/src/main/res/values-zh-rHK/strings.xml @@ -100,15 +100,15 @@ 已自動驗證電話號碼 重新傳送驗證碼 驗證電話號碼 - Use a different phone number + 使用其他電話號碼 輕觸 [%1$s] 後,系統將會傳送一封簡訊。您可能需支付簡訊和數據傳輸費用。 輕觸 [%1$s] 即表示您同意接受我們的《%2$s》和《%3$s》。系統將會傳送簡訊給您,不過您可能需要支付簡訊和數據傳輸費用。 - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + 驗證錯誤 + 重試 + 需要額外驗證。請完成雙重驗證。 + 需要連結帳戶。請嘗試其他登入方式。 + 驗證已取消。準備好後請再試一次。 選擇驗證方法 @@ -166,7 +166,7 @@ 重新發送驗證電郵 密鑰 登出 - 登入身分 + 登入身分:%1$s 略過 使用其他方式 驗證碼 @@ -176,4 +176,17 @@ 多重身份验证当前已禁用 + 電郵地址或密碼不正確 + 此驗證程序已失效,請重新要求驗證碼。 + 電話驗證尚未完成,請再試一次。 + 這些憑證屬於其他帳戶。 + 此電話號碼並未在此帳戶中設定為驗證方法。 + 您的登入程序已逾時,請重新登入以繼續。 + 此連結已失效,請要求新的連結。 + 請先驗證您的電郵地址,然後再繼續。 + 此驗證方法已在此帳戶中設定。 + 您已達到此帳戶的驗證方法數量上限。 + 您的密碼不符合要求,請嘗試其他密碼。 + 密碼太長。長度上限為 %1$d。 + 找不到此帳戶的密碼金鑰,請使用其他方式登入。 diff --git a/auth/src/main/res/values-zh-rTW/strings.xml b/auth/src/main/res/values-zh-rTW/strings.xml index 63b993c5b7..ec7d2e37e9 100755 --- a/auth/src/main/res/values-zh-rTW/strings.xml +++ b/auth/src/main/res/values-zh-rTW/strings.xml @@ -100,15 +100,15 @@ 已自動驗證電話號碼 重新傳送驗證碼 驗證電話號碼 - Use a different phone number + 使用其他電話號碼 輕觸 [%1$s] 後,系統將會傳送一封簡訊。您可能需支付簡訊和數據傳輸費用。 輕觸 [%1$s] 即表示您同意接受我們的《%2$s》和《%3$s》。系統將會傳送簡訊給您,不過您可能需要支付簡訊和數據傳輸費用。 - Authentication Error - Try again - Additional verification required. Please complete multi-factor authentication. - Account needs to be linked. Please try a different sign-in method. - Authentication was cancelled. Please try again when ready. + 驗證錯誤 + 重試 + 需要額外驗證。請完成雙重驗證。 + 需要連結帳戶。請嘗試其他登入方式。 + 驗證已取消。準備好後請再試一次。 選擇驗證方法 @@ -166,7 +166,7 @@ 重新傳送驗證郵件 密鑰 登出 - 登入身分 + 登入身分:%1$s 略過 使用其他方式 驗證碼 @@ -176,4 +176,17 @@ 多重身份验证当前已禁用 + 電子郵件地址或密碼不正確 + 此驗證工作階段已失效,請重新要求驗證碼。 + 電話驗證尚未完成,請再試一次。 + 這些憑證屬於其他帳號。 + 此電話號碼並未在這個帳號中設定為驗證方法。 + 您的登入工作階段已過期,請重新登入以繼續。 + 此連結已失效,請要求新的連結。 + 請先驗證您的電子郵件地址,然後再繼續。 + 此驗證方法已在這個帳號中設定。 + 您已達到這個帳號的驗證方法數量上限。 + 您的密碼不符合要求,請嘗試其他密碼。 + 密碼太長。長度上限為 %1$d。 + 找不到此帳戶的密碼金鑰,請使用其他方式登入。 diff --git a/auth/src/main/res/values-zh/strings.xml b/auth/src/main/res/values-zh/strings.xml index ada6243e74..05d4ad3f5c 100755 --- a/auth/src/main/res/values-zh/strings.xml +++ b/auth/src/main/res/values-zh/strings.xml @@ -100,7 +100,7 @@ 电话号码已自动验证 重新发送验证码 验证电话号码 - Use a different phone number + 使用其他电话号码 您点按“%1$s”后,系统会向您发送一条短信。这可能会产生短信费用和上网流量费。 点按"%1$s"即表示您接受我们的%2$s和%3$s。系统会向您发送一条短信。这可能会产生短信费用和上网流量费。 身份验证错误 @@ -165,7 +165,7 @@ 重新发送验证邮件 密钥 退出 - 登录身份 + 登录身份:%1$s 跳过 使用其他方式 验证码 @@ -175,4 +175,17 @@ 多重身份验证当前已禁用 + 电子邮件地址或密码不正确 + 该验证会话已失效,请重新获取验证码。 + 电话号码验证未完成,请重试。 + 这些凭据属于其他帐号。 + 该电话号码未在此帐号中设置为验证方式。 + 您的登录会话已过期,请重新登录以继续。 + 该链接已失效,请重新获取。 + 请先验证您的电子邮件地址,然后再继续。 + 该验证方式已在此帐号中设置。 + 您已达到此帐号的验证方式数量上限。 + 您的密码不符合要求,请尝试其他密码。 + 密码过长。最大长度为 %1$d。 + 未找到此账号的通行密钥,请通过其他方式登录。 diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml index ca6738c108..6bc4becc12 100644 --- a/auth/src/main/res/values/strings.xml +++ b/auth/src/main/res/values/strings.xml @@ -3,26 +3,26 @@ @string/app_name - Loading... + Loading… Initializing - Signing in as guest... - Signing in with Google... - Signing in with Facebook... - Signing in with %1$s... - Verifying phone number... - Submitting verification code... - Signing in with phone... - Creating account... - Signing in... - Signing in... - Sending sign-in link... - Signing in with email link... - Sending password reset email... - Signing out... - Finishing that action... - Deleting account... + Signing in as guest… + Signing in with Google… + Signing in with Facebook… + Signing in with %1$s + Verifying phone number… + Submitting verification code… + Signing in with phone… + Creating account… + Signing in… + Signing in… + Sending sign-in link… + Signing in with email link… + Sending password reset email… + Signing out… + Finishing that action… + Deleting account… Sign in Continue By continuing, you are indicating that you accept our %1$s and %2$s. @@ -132,13 +132,13 @@ That email address isn\'t correct Enter your email address to continue Please enter a first and last name. - Checking for existing accounts... + Checking for existing accounts… Sign up First & last name Save - Signing up... + Signing up… Password not strong enough. Use at least %1$d character and a mix of letters and numbers Password not strong enough. Use at least %1$d characters and a mix of letters and numbers @@ -167,7 +167,7 @@ You\'ve already used %1$s. You can connect your %2$s account with %1$s by signing in with email link below.\n\nFor this flow to successfully connect your %2$s account with this email, you have to open the link on the same device or browser. - Signing in... + Signing in… Trouble signing in? @@ -177,7 +177,7 @@ reset your password. Send Follow the instructions sent to %1$s to recover your password. - Sending... + Sending… That email address doesn\'t match an existing account @@ -194,7 +194,14 @@ Passwords do not match - Password must be at least %1$d characters long + + Password must be at least %1$d characters long + + Password is too long. The maximum length is %1$d. Password must contain at least one uppercase letter Password must contain at least one lowercase letter Password must contain at least one number @@ -230,7 +237,7 @@ Enter the 6-digit code we sent to Resend code in %1$s Verify your phone number - Verifying... + Verifying… Wrong code. Try again. This phone number has been used too many times There was a problem verifying your phone number @@ -252,20 +259,35 @@ User account has been disabled - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + That email or password isn\'t correct + That verification session is no longer valid. Request a new code. + Phone verification didn\'t complete. Try again. + Those credentials belong to a different account. + That phone number isn\'t set up for verification on this account. + Your sign-in session expired. Sign in again to continue. + That link is no longer valid. Request a new one. + Verify your email address before you continue. + That verification method is already set up on this account. + You\'ve reached the limit for verification methods on this account. + Your password doesn\'t meet the requirements. Try a different one. + We couldn\'t find a passkey for this account. Sign in another way. Choose Authentication Method diff --git a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionRecoveryResolutionTest.kt b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionRecoveryResolutionTest.kt new file mode 100644 index 0000000000..d3c2835671 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionRecoveryResolutionTest.kt @@ -0,0 +1,516 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.ui.components.getRecoveryActionText +import com.firebase.ui.auth.ui.components.getRecoveryMessage +import com.firebase.ui.auth.ui.components.isRecoverable +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.firebase.FirebaseException +import com.google.firebase.FirebaseTooManyRequestsException +import com.google.firebase.auth.FirebaseAuthException +import com.google.firebase.auth.FirebaseAuthInvalidUserException +import com.google.firebase.auth.FirebaseAuthMissingActivityForRecaptchaException +import com.google.firebase.auth.FirebaseAuthMultiFactorException +import com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException +import com.google.firebase.auth.FirebaseAuthUserCollisionException +import com.google.firebase.auth.FirebaseAuthWeakPasswordException +import java.util.Locale +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.kotlin.doCallRealMethod +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * End-to-end resolution tests: a real Firebase exception goes through [AuthException.from] with a + * real [DefaultAuthUIStringProvider], and the result is rendered through + * [getRecoveryMessage] exactly as the error dialog would render it. + * + * Why the real provider and not a mock: the `fui_error_*` type-level hooks are deliberately blank + * so hosts can override them, so a mocked provider that stubs one proves nothing about what ships. + * Every assertion here fails if a branch of `from()` falls through a blank hook to the raw, + * untranslated Firebase SDK diagnostic. + * + * @suppress Internal test class + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class AuthExceptionRecoveryResolutionTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + private val strings: AuthUIStringProvider = DefaultAuthUIStringProvider(context) + + /** Renders [firebaseException] the way the error dialog does, end to end. */ + private fun resolve( + firebaseException: Exception, + provider: AuthUIStringProvider = strings + ): String = getRecoveryMessage(AuthException.from(firebaseException, provider), provider) + + // Verbatim firebase-auth 24.2.0 diagnostics. All are English regardless of device locale. + private val networkDiagnostic = "A network error (such as timeout, interrupted connection or " + + "unreachable host) has occurred." + private val userNotFoundDiagnostic = "There is no user record corresponding to this " + + "identifier. The user may have been deleted." + private val weakPasswordDiagnostic = "The given password is invalid. [ Password should be at " + + "least 6 characters ]" + private val emailInUseDiagnostic = "The email address is already in use by another account." + private val mfaDiagnostic = "Please complete a second factor challenge." + private val recentLoginDiagnostic = "This operation is sensitive and requires recent " + + "authentication. Log in again before retrying this request." + private val cancelledDiagnostic = "User cancelled the sign-in flow." + private val operationNotAllowedDiagnostic = "This operation is not allowed. This may be " + + "because the given sign-in provider is disabled for this Firebase project. Enable it " + + "in the Firebase console, under the sign-in method tab of the Auth section. " + + "[ OPERATION_NOT_ALLOWED ]" + + private fun userCollision(code: String, email: String? = null): FirebaseAuthUserCollisionException { + val exception = mock(FirebaseAuthUserCollisionException::class.java) + whenever(exception.errorCode).thenReturn(code) + whenever(exception.message).thenReturn(emailInUseDiagnostic) + whenever(exception.email).thenReturn(email) + return exception + } + + private fun multiFactor(): FirebaseAuthMultiFactorException { + val exception = mock(FirebaseAuthMultiFactorException::class.java) + whenever(exception.message).thenReturn(mfaDiagnostic) + return exception + } + + // ============================================================================================= + // The six types whose type-level hook is deliberately blank + // ============================================================================================= + + @Test + fun `network failure resolves to the library's own copy, not the SDK diagnostic`() { + val firebaseException = object : FirebaseException(networkDiagnostic) {} + + val resolved = resolve(firebaseException) + + assertThat(resolved).isEqualTo(strings.networkErrorRecoveryMessage) + assertThat(resolved).isNotEqualTo(networkDiagnostic) + } + + @Test + fun `user not found resolves to the library's own copy, not the SDK diagnostic`() { + val firebaseException = + FirebaseAuthInvalidUserException("ERROR_USER_NOT_FOUND", userNotFoundDiagnostic) + + val resolved = resolve(firebaseException) + + assertThat(resolved).isEqualTo(strings.userNotFoundRecoveryMessage) + assertThat(resolved).isNotEqualTo(userNotFoundDiagnostic) + } + + @Test + fun `an unmapped user code resolves to the library's own copy, not the SDK diagnostic`() { + val firebaseException = + FirebaseAuthInvalidUserException("ERROR_SOMETHING_NEW", userNotFoundDiagnostic) + + val resolved = resolve(firebaseException) + + assertThat(resolved).isEqualTo(strings.userNotFoundRecoveryMessage) + assertThat(resolved).isNotEqualTo(userNotFoundDiagnostic) + } + + @Test + fun `weak password resolves to the library's own copy, not the SDK diagnostic`() { + val firebaseException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + weakPasswordDiagnostic, + "Password should be at least 6 characters" + ) + + val authException = AuthException.from(firebaseException, strings) + + assertThat(authException.message).isEqualTo(strings.weakPasswordRecoveryMessage) + assertThat(authException.message).isNotEqualTo(weakPasswordDiagnostic) + assertThat(resolve(firebaseException)).startsWith(strings.weakPasswordRecoveryMessage) + } + + @Test + fun `email already in use resolves to the library's own copy, not the SDK diagnostic`() { + val firebaseException = userCollision("ERROR_EMAIL_ALREADY_IN_USE") + + val resolved = resolve(firebaseException) + + assertThat(resolved).isEqualTo(strings.emailAlreadyInUseRecoveryMessage) + assertThat(resolved).isNotEqualTo(emailInUseDiagnostic) + } + + @Test + fun `mfa required resolves to the library's own copy, not the SDK diagnostic`() { + val firebaseException = multiFactor() + + val resolved = resolve(firebaseException) + + assertThat(resolved).isEqualTo(strings.mfaRequiredRecoveryMessage) + assertThat(resolved).isNotEqualTo(mfaDiagnostic) + } + + @Test + fun `auth cancelled resolves to the library's own copy, not the SDK diagnostic`() { + for (code in listOf("ERROR_USER_CANCELLED", "ERROR_WEB_CONTEXT_CANCELED")) { + val firebaseException = object : FirebaseAuthException(code, cancelledDiagnostic) {} + + val resolved = resolve(firebaseException) + + assertWithMessage(code).that(resolved).isEqualTo(strings.authCancelledRecoveryMessage) + assertWithMessage(code).that(resolved).isNotEqualTo(cancelledDiagnostic) + } + } + + @Test + fun `too many requests resolves to the library's own copy, not the SDK diagnostic`() { + val diagnostic = "We have blocked all requests from this device due to unusual activity." + val firebaseException = FirebaseTooManyRequestsException(diagnostic) + + val resolved = resolve(firebaseException) + + assertThat(resolved).isEqualTo(strings.tooManyRequestsRecoveryMessage) + assertThat(resolved).isNotEqualTo(diagnostic) + } + + @Test + fun `account collision resolves to the library's own copy, not the SDK diagnostic`() { + val codes = listOf( + "ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL", + "ERROR_CREDENTIAL_ALREADY_IN_USE", + "ERROR_SOME_FUTURE_COLLISION_CODE", + ) + + for (code in codes) { + val resolved = resolve(userCollision(code)) + + assertWithMessage(code).that(resolved) + .isEqualTo(strings.accountLinkingRequiredRecoveryMessage) + assertWithMessage(code).that(resolved).isNotEqualTo(emailInUseDiagnostic) + } + } + + // ============================================================================================= + // Misconfiguration — the diagnostic must not be on the message at all + // ============================================================================================= + + @Test + fun `a disabled sign-in provider never puts the console diagnostic on the message`() { + val firebaseException = + object : FirebaseAuthException("ERROR_OPERATION_NOT_ALLOWED", operationNotAllowedDiagnostic) {} + + val authException = AuthException.from(firebaseException, strings) + + assertThat(authException).isInstanceOf(AuthException.MisconfigurationException::class.java) + // EmailAuthScreen and PhoneAuthScreen render this inline, bypassing getRecoveryMessage. + assertThat(authException.message).isEqualTo(strings.unknownErrorRecoveryMessage) + assertThat(authException.message).doesNotContain("Firebase") + assertThat(authException.message).doesNotContain("OPERATION_NOT_ALLOWED") + // The diagnostic is still there for logs and for hosts. + assertThat(authException.cause).isEqualTo(firebaseException) + assertThat(authException.cause?.message).isEqualTo(operationNotAllowedDiagnostic) + assertThat(resolve(firebaseException)).isEqualTo(strings.unknownErrorRecoveryMessage) + } + + @Test + fun `the email-template and quota codes are misconfiguration, not unknown errors`() { + val codes = listOf( + "ERROR_INVALID_MESSAGE_PAYLOAD", + "ERROR_INVALID_SENDER", + "ERROR_INVALID_RECIPIENT_EMAIL", + // Declared on FirebaseAuthMissingActivityForRecaptchaException's own constructor. + "ERROR_MISSING_ACTIVITY", + "ERROR_WEB_STORAGE_UNSUPPORTED", + "ERROR_QUOTA_EXCEEDED", + ) + + for (code in codes) { + val diagnostic = "Raw SDK English naming $code." + val firebaseException = object : FirebaseAuthException(code, diagnostic) {} + val authException = AuthException.from(firebaseException, strings) + + assertWithMessage(code).that(authException) + .isInstanceOf(AuthException.MisconfigurationException::class.java) + assertWithMessage(code).that(authException.message).isNotEqualTo(diagnostic) + assertWithMessage(code).that(authException.cause?.message).isEqualTo(diagnostic) + } + } + + @Test + fun `the SDK's own missing-activity exception type reaches the misconfiguration arm`() { + // The synthetic assertions elsewhere pin the `when` arm; this pins that the SDK's own + // type still reaches it, which an SDK release reparenting it would break silently. + val firebaseException = FirebaseAuthMissingActivityForRecaptchaException() + + val authException = AuthException.from(firebaseException, strings) + + assertThat(firebaseException.errorCode).isEqualTo("ERROR_MISSING_ACTIVITY") + assertThat(authException) + .isInstanceOf(AuthException.MisconfigurationException::class.java) + // Retrying cannot conjure the Activity the host never supplied. + assertThat(isRecoverable(authException)).isFalse() + assertThat(authException.message).isEqualTo(strings.unknownErrorRecoveryMessage) + assertThat(authException.message).doesNotContain("Recaptcha") + // The SDK's own English stays on the cause, where logs still reach it. + assertThat(authException.cause).isEqualTo(firebaseException) + assertThat(authException.cause?.message).contains("valid Activity is required") + } + + // ============================================================================================= + // Blanket invariant + // ============================================================================================= + + @Test + fun `no Firebase error code resolves to the raw SDK diagnostic`() { + // One representative code per named arm of the FirebaseAuthInvalidCredentialsException + // branch of from(), then, below the blank line, the codes that fall through to its `else`. + val codes = listOf( + "ERROR_INVALID_CREDENTIAL", + "ERROR_WRONG_PASSWORD", + "ERROR_INVALID_EMAIL", + "ERROR_MISSING_EMAIL", + "ERROR_MISSING_PASSWORD", + "ERROR_INVALID_PHONE_NUMBER", + "ERROR_MISSING_PHONE_NUMBER", + "ERROR_INVALID_VERIFICATION_CODE", + "ERROR_SESSION_EXPIRED", + "ERROR_INVALID_VERIFICATION_ID", + "ERROR_RETRY_PHONE_AUTH", + "ERROR_USER_MISMATCH", + "ERROR_PHONE_NUMBER_NOT_FOUND", + "ERROR_MULTI_FACTOR_INFO_NOT_FOUND", + "ERROR_MISSING_MULTI_FACTOR_INFO", + "ERROR_INVALID_MULTI_FACTOR_SESSION", + "ERROR_INVALID_CUSTOM_TOKEN", + "ERROR_MISSING_OR_INVALID_NONCE", + "ERROR_INVALID_AUTHENTICATOR_RESPONSE", + "ERROR_PASSKEY_ENROLLMENT_NOT_FOUND", + + // The first ships in firebase-auth 24.2.0; the second stands in for a future code. + "ERROR_REJECTED_CREDENTIAL", + "ERROR_SOME_FUTURE_CREDENTIAL_CODE", + ) + val diagnostic = "The Firebase SDK's own untranslated English." + + for (code in codes) { + val firebaseException = + com.google.firebase.auth.FirebaseAuthInvalidCredentialsException(code, diagnostic) + assertWithMessage(code).that(resolve(firebaseException)).isNotEqualTo(diagnostic) + assertWithMessage(code).that(AuthException.from(firebaseException, strings).message) + .isNotEqualTo(diagnostic) + } + } + + @Test + fun `no plain auth error code resolves to the raw SDK diagnostic`() { + val codes = listOf( + "ERROR_UNVERIFIED_EMAIL", + "ERROR_SECOND_FACTOR_ALREADY_ENROLLED", + "ERROR_MAXIMUM_SECOND_FACTOR_COUNT_EXCEEDED", + "ERROR_OPERATION_NOT_ALLOWED", + "ERROR_UNAUTHORIZED_DOMAIN", + "ERROR_INVALID_CERT_HASH", + "ERROR_RECAPTCHA_NOT_ENABLED", + "ERROR_QUOTA_EXCEEDED", + // The else branch: neither user-facing nor a known setup problem. + "INTERNAL_ERROR", + "ERROR_WEB_INTERNAL_ERROR", + "ERROR_SOME_FUTURE_AUTH_CODE", + ) + val diagnostic = "The Firebase SDK's own untranslated English." + + for (code in codes) { + val firebaseException = object : FirebaseAuthException(code, diagnostic) {} + assertWithMessage(code).that(resolve(firebaseException)).isNotEqualTo(diagnostic) + assertWithMessage(code).that(AuthException.from(firebaseException, strings).message) + .isNotEqualTo(diagnostic) + } + } + + // ============================================================================================= + // The copy is actually translated, not just library-owned + // ============================================================================================= + + + + @Test + fun `a French device changing its email sees French, not the English reauth diagnostic`() { + val french = DefaultAuthUIStringProvider(context, Locale.FRENCH) + val firebaseException = FirebaseAuthRecentLoginRequiredException( + "ERROR_REQUIRES_RECENT_LOGIN", + recentLoginDiagnostic + ) + + val resolved = resolve(firebaseException, french) + + // `errorRecentLoginRequired` ships blank, so the arm falls to the MFA string. + assertThat(resolved).isEqualTo(french.mfaErrorRecentLoginRequired) + assertThat(resolved).isNotEqualTo(recentLoginDiagnostic) + assertThat(resolved).isNotEqualTo(strings.mfaErrorRecentLoginRequired) + } + + @Test + fun `reauthentication required resolves to library copy, not the SDK diagnostic`() { + val firebaseException = FirebaseAuthRecentLoginRequiredException( + "ERROR_REQUIRES_RECENT_LOGIN", + recentLoginDiagnostic + ) + + val result = AuthException.from(firebaseException, strings) + + assertThat(result).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(result.message).isEqualTo(strings.mfaErrorRecentLoginRequired) + assertThat(result.cause?.message).isEqualTo(recentLoginDiagnostic) + assertThat(resolve(firebaseException)).isEqualTo(strings.mfaErrorRecentLoginRequired) + } + + // ============================================================================================= + // Developer-setup faults in the invalid-credential family + // ============================================================================================= + + @Test + fun `a bad Sign in with Apple nonce is reported as a misconfiguration, not a bad password`() { + val diagnostic = "The supplied auth credential is malformed, has expired or is " + + "currently unsupported. [ MISSING_OR_INVALID_NONCE ]" + + for (code in listOf("ERROR_MISSING_OR_INVALID_NONCE", "ERROR_INVALID_AUTHENTICATOR_RESPONSE")) { + val firebaseException = + com.google.firebase.auth.FirebaseAuthInvalidCredentialsException(code, diagnostic) + val result = AuthException.from(firebaseException, strings) + + // The host built the federated request wrong, so this must not be recoverable. + assertWithMessage(code).that(result) + .isInstanceOf(AuthException.MisconfigurationException::class.java) + assertWithMessage(code).that(result.message) + .isEqualTo(strings.unknownErrorRecoveryMessage) + assertWithMessage(code).that(result.cause?.message).isEqualTo(diagnostic) + } + } + + @Test + fun `unnamed invalid-credential codes stay recoverable but never show the SDK diagnostic`() { + val diagnostic = "The Firebase SDK's own untranslated English." + + for (code in listOf("ERROR_REJECTED_CREDENTIAL", "ERROR_SOME_FUTURE_CREDENTIAL_CODE")) { + val firebaseException = + com.google.firebase.auth.FirebaseAuthInvalidCredentialsException(code, diagnostic) + val result = AuthException.from(firebaseException, strings) + + // "Mismatching credentials" also covers the wrong account, which signing in fixes. + assertWithMessage(code).that(result) + .isInstanceOf(AuthException.InvalidCredentialsException::class.java) + // But the copy has to be generic — we do not know what the code means. + assertWithMessage(code).that(result.message) + .isEqualTo(strings.unknownErrorRecoveryMessage) + assertWithMessage(code).that(result.message).isNotEqualTo(diagnostic) + assertWithMessage(code).that(result.cause?.message).isEqualTo(diagnostic) + } + } + + @Test + fun `a missing passkey enrolment points at another sign-in method, not a futile retry`() { + val diagnostic = "Cannot find the passkey linked to the current account." + val firebaseException = com.google.firebase.auth.FirebaseAuthInvalidCredentialsException( + "ERROR_PASSKEY_ENROLLMENT_NOT_FOUND", diagnostic + ) + + val result = AuthException.from(firebaseException, strings) + + // Not InvalidCredentialsException: that is recoverable, so the dialog would offer a retry. + assertThat(result).isInstanceOf(AuthException.SignInMethodUnavailableException::class.java) + assertThat(result).isNotInstanceOf(AuthException.InvalidCredentialsException::class.java) + // Specific copy, not the generic unknown-error string. + assertThat(result.message).isEqualTo(strings.errorPasskeyNotFound) + assertThat(result.message).isNotEqualTo(strings.unknownErrorRecoveryMessage) + assertThat(result.message).isNotEqualTo(diagnostic) + assertThat(result.cause?.message).isEqualTo(diagnostic) + } + + @Test + fun `the dialog offers no retry action for a missing passkey enrolment`() { + val firebaseException = com.google.firebase.auth.FirebaseAuthInvalidCredentialsException( + "ERROR_PASSKEY_ENROLLMENT_NOT_FOUND", + "Cannot find the passkey linked to the current account." + ) + + val result = AuthException.from(firebaseException, strings) + + // The dialog renders the action button only when isRecoverable is true. + assertThat(isRecoverable(result)).isFalse() + // And the text itself is not a retry invitation, for any caller reading it directly. + assertThat(getRecoveryActionText(result, strings)).isNotEqualTo(strings.retryAction) + assertThat(getRecoveryActionText(result, strings)).isEqualTo(strings.dismissAction) + // The body still says the useful thing. + assertThat(getRecoveryMessage(result, strings)).isEqualTo(strings.errorPasskeyNotFound) + } + + // ============================================================================================= + // A host's own hook still wins + // ============================================================================================= + + @Test + fun `a host that fills the type-level hook still overrides the generic recovery copy`() { + val hostStrings = mock(AuthUIStringProvider::class.java) + whenever(hostStrings.errorNetworkGeneric).thenReturn("Host network copy") + whenever(hostStrings.networkErrorRecoveryMessage).thenReturn("Generic network copy") + + val result = AuthException.from(object : FirebaseException(networkDiagnostic) {}, hostStrings) + + assertThat(result.message).isEqualTo("Host network copy") + } + + @Test + fun `a host's credential copy does not hijack the missing-passkey message`() { + // `errorInvalidCredentials` is the hook for a type this arm deliberately does not return, + // so a host overriding both must see its passkey copy, not its password copy. + val hostStrings = mock(AuthUIStringProvider::class.java) + whenever(hostStrings.errorInvalidCredentials) + .thenReturn("Check your password and try again.") + whenever(hostStrings.errorPasskeyNotFound).thenReturn("Use another way to sign in.") + + val result = AuthException.from( + com.google.firebase.auth.FirebaseAuthInvalidCredentialsException( + "ERROR_PASSKEY_ENROLLMENT_NOT_FOUND", + "Cannot find the passkey linked to the current account." + ), + hostStrings + ) + + assertThat(result).isInstanceOf(AuthException.SignInMethodUnavailableException::class.java) + assertThat(result.message).isEqualTo("Use another way to sign in.") + assertThat(result.message).isNotEqualTo("Check your password and try again.") + } + + @Test + fun `a host that leaves the passkey hook unset gets the generic string, not credential copy`() { + // The interface default is what a host implementing AuthUIStringProvider directly sees; + // credential copy there would sit in a dialog with no retry button. + val hostStrings = mock(AuthUIStringProvider::class.java) + doCallRealMethod().whenever(hostStrings).errorPasskeyNotFound + whenever(hostStrings.errorInvalidCredentials).thenReturn("Check your password and try again.") + whenever(hostStrings.errorUnknownAuth).thenReturn("Something went wrong. Please try later.") + + assertThat(hostStrings.errorPasskeyNotFound) + .isEqualTo("Something went wrong. Please try later.") + assertThat(hostStrings.errorPasskeyNotFound) + .isNotEqualTo("Check your password and try again.") + } +} diff --git a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt index ea8ec7ecd9..47d4f07a13 100644 --- a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt @@ -16,8 +16,12 @@ package com.firebase.ui.auth import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.FirebaseException +import com.google.firebase.FirebaseTooManyRequestsException +import com.google.firebase.auth.FirebaseAuthActionCodeException import com.google.firebase.auth.FirebaseAuthException +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.FirebaseAuthInvalidUserException import com.google.firebase.auth.FirebaseAuthWeakPasswordException import org.junit.Test @@ -52,19 +56,35 @@ class AuthExceptionTest { } @Test - fun `from() maps FirebaseAuthException with ERROR_TOO_MANY_REQUESTS to TooManyRequestsException`() { - // Arrange - val firebaseException = object : FirebaseAuthException("ERROR_TOO_MANY_REQUESTS", "Too many requests") {} + fun `from() maps FirebaseTooManyRequestsException to TooManyRequestsException`() { + // Arrange — rate limiting arrives as this type, not as a FirebaseAuthException. It carries + // no error code, so the only thing that can select the arm is the exception class itself. + val firebaseException = FirebaseTooManyRequestsException( + "We have blocked all requests from this device due to unusual activity. Try again later." + ) // Act val authException = AuthException.from(firebaseException) - // Assert + // Assert — without a dedicated arm this falls through to `is FirebaseException` and a + // throttled user is told they have no internet connection. assertThat(authException).isInstanceOf(AuthException.TooManyRequestsException::class.java) - assertThat(authException.message).isEqualTo("Too many requests") assertThat(authException.cause).isEqualTo(firebaseException) } + @Test + fun `from() takes the too-many-requests message from the string provider`() { + val firebaseException = FirebaseTooManyRequestsException("Blocked due to unusual activity.") + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorTooManyRequests).thenReturn("Zu viele Versuche") + + val result = AuthException.from(firebaseException, stringProvider) + + assertThat(result).isInstanceOf(AuthException.TooManyRequestsException::class.java) + assertThat(result.message).isEqualTo("Zu viele Versuche") + } + + @Test fun `from() maps FirebaseAuthException with unknown error code to UnknownException`() { // Arrange @@ -289,4 +309,288 @@ class AuthExceptionTest { assertThat(exception.failingRequirements).isEqualTo(requirements) } + + // ============================================================================================= + // Per-error-code message selection + // + // Every code below is one the resolved firebase-auth 24.2.0 maps onto the exception type the + // arm matches, so each branch is reachable. The provider member is stubbed to a sentinel that + // exists nowhere else: collapsing two codes onto one branch, or dropping a branch back to the + // arm's generic `else`, changes the message and fails the test. + // ============================================================================================= + + /** The raw text the SDK would put on the exception, which must lose to the provider string. */ + private val sdkText = "The Firebase SDK's own untranslated English." + + private fun invalidCredentials(errorCode: String) = + FirebaseAuthInvalidCredentialsException(errorCode, sdkText) + + @Test + fun `from() routes each invalid-credentials error code to its own string`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorIncorrectEmailOrPassword).thenReturn("s:incorrectEmailOrPassword") + whenever(stringProvider.invalidPassword).thenReturn("s:invalidPassword") + whenever(stringProvider.invalidEmailAddress).thenReturn("s:invalidEmailAddress") + whenever(stringProvider.missingEmailAddress).thenReturn("s:missingEmailAddress") + whenever(stringProvider.requiredField).thenReturn("s:requiredField") + whenever(stringProvider.invalidPhoneNumber).thenReturn("s:invalidPhoneNumber") + whenever(stringProvider.missingPhoneNumber).thenReturn("s:missingPhoneNumber") + whenever(stringProvider.invalidVerificationCode).thenReturn("s:invalidVerificationCode") + whenever(stringProvider.errorSessionExpired).thenReturn("s:sessionExpired") + whenever(stringProvider.errorInvalidVerificationId).thenReturn("s:invalidVerificationId") + whenever(stringProvider.errorRetryPhoneAuth).thenReturn("s:retryPhoneAuth") + whenever(stringProvider.errorUserMismatch).thenReturn("s:userMismatch") + whenever(stringProvider.errorPhoneNumberNotEnrolled).thenReturn("s:phoneNumberNotEnrolled") + whenever(stringProvider.errorMultiFactorSessionExpired).thenReturn("s:multiFactorSessionExpired") + + val expected = mapOf( + "ERROR_INVALID_CREDENTIAL" to "s:incorrectEmailOrPassword", + "ERROR_WRONG_PASSWORD" to "s:invalidPassword", + "ERROR_INVALID_EMAIL" to "s:invalidEmailAddress", + "ERROR_MISSING_EMAIL" to "s:missingEmailAddress", + "ERROR_MISSING_PASSWORD" to "s:requiredField", + "ERROR_MISSING_VERIFICATION_CODE" to "s:requiredField", + "ERROR_INVALID_PHONE_NUMBER" to "s:invalidPhoneNumber", + "ERROR_MISSING_PHONE_NUMBER" to "s:missingPhoneNumber", + "ERROR_INVALID_VERIFICATION_CODE" to "s:invalidVerificationCode", + "ERROR_SESSION_EXPIRED" to "s:sessionExpired", + "ERROR_INVALID_VERIFICATION_ID" to "s:invalidVerificationId", + "ERROR_MISSING_VERIFICATION_ID" to "s:invalidVerificationId", + "ERROR_RETRY_PHONE_AUTH" to "s:retryPhoneAuth", + "ERROR_USER_MISMATCH" to "s:userMismatch", + "ERROR_PHONE_NUMBER_NOT_FOUND" to "s:phoneNumberNotEnrolled", + "ERROR_MULTI_FACTOR_INFO_NOT_FOUND" to "s:phoneNumberNotEnrolled", + "ERROR_INVALID_MULTI_FACTOR_SESSION" to "s:multiFactorSessionExpired", + "ERROR_MISSING_MULTI_FACTOR_SESSION" to "s:multiFactorSessionExpired", + ) + + val actual = expected.keys.associateWith { code -> + val result = AuthException.from(invalidCredentials(code), stringProvider) + assertThat(result).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + result.message + } + + assertThat(actual).containsExactlyEntriesIn(expected) + } + + @Test + fun `from() uses ERROR_INVALID_CREDENTIAL copy that does not blame the password`() { + // Under email enumeration protection this single code covers wrong password AND no such + // account, so reusing the wrong-password string would state something false. + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorIncorrectEmailOrPassword).thenReturn("Email or password wrong") + whenever(stringProvider.invalidPassword).thenReturn("Incorrect password.") + + val result = AuthException.from(invalidCredentials("ERROR_INVALID_CREDENTIAL"), stringProvider) + + assertThat(result.message).isEqualTo("Email or password wrong") + } + + @Test + fun `from() prefers the blank-able type-level hook over the per-code string`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorInvalidCredentials).thenReturn("Host-wide override") + whenever(stringProvider.invalidPassword).thenReturn("Incorrect password.") + + val result = AuthException.from(invalidCredentials("ERROR_WRONG_PASSWORD"), stringProvider) + + assertThat(result.message).isEqualTo("Host-wide override") + } + + @Test + fun `from() falls back to the Firebase message when the per-code string is blank`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorInvalidCredentials).thenReturn("") + whenever(stringProvider.invalidPassword).thenReturn("") + + val result = AuthException.from(invalidCredentials("ERROR_WRONG_PASSWORD"), stringProvider) + + assertThat(result.message).isEqualTo(sdkText) + } + + @Test + fun `from() routes custom token codes to MisconfigurationException`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.unknownErrorRecoveryMessage).thenReturn("Une erreur est survenue") + + for (code in listOf("ERROR_INVALID_CUSTOM_TOKEN", "ERROR_CUSTOM_TOKEN_MISMATCH")) { + val firebaseException = invalidCredentials(code) + val result = AuthException.from(firebaseException, stringProvider) + assertThat(result).isInstanceOf(AuthException.MisconfigurationException::class.java) + // The diagnostic names the developer's own token backend; it is renderable nowhere. + assertWithMessage(code).that(result.message).isEqualTo("Une erreur est survenue") + assertWithMessage(code).that(result.cause).isEqualTo(firebaseException) + assertWithMessage(code).that(result.cause?.message).isEqualTo(sdkText) + } + } + + @Test + fun `from() routes expired user tokens to the session-expired copy`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorMultiFactorSessionExpired).thenReturn("Session gone") + + for (code in listOf("ERROR_INVALID_USER_TOKEN", "ERROR_USER_TOKEN_EXPIRED")) { + val firebaseException = FirebaseAuthInvalidUserException(code, sdkText) + val result = AuthException.from(firebaseException, stringProvider) + assertThat(result).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(result.message).isEqualTo("Session gone") + } + } + + @Test + fun `from() routes action code failures to the action-code copy`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorActionCodeInvalid).thenReturn("Link no longer valid") + + for (code in listOf("ERROR_EXPIRED_ACTION_CODE", "ERROR_INVALID_ACTION_CODE")) { + val firebaseException = FirebaseAuthActionCodeException(code, sdkText) + val result = AuthException.from(firebaseException, stringProvider) + assertThat(result).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(result.message).isEqualTo("Link no longer valid") + } + } + + @Test + fun `from() routes the user-facing plain auth codes to their own strings`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorUnverifiedEmail).thenReturn("s:unverifiedEmail") + whenever(stringProvider.errorSecondFactorAlreadyEnrolled).thenReturn("s:alreadyEnrolled") + whenever(stringProvider.errorMaximumSecondFactorCountExceeded).thenReturn("s:maxFactors") + + val expected = mapOf( + "ERROR_UNVERIFIED_EMAIL" to "s:unverifiedEmail", + "ERROR_SECOND_FACTOR_ALREADY_ENROLLED" to "s:alreadyEnrolled", + "ERROR_MAXIMUM_SECOND_FACTOR_COUNT_EXCEEDED" to "s:maxFactors", + ) + + val actual = expected.keys.associateWith { code -> + val result = AuthException.from(object : FirebaseAuthException(code, sdkText) {}, stringProvider) + assertThat(result).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + result.message + } + + assertThat(actual).containsExactlyEntriesIn(expected) + } + + @Test + fun `from() routes developer setup codes to MisconfigurationException, diagnostic on the cause`() { + val configurationCodes = listOf( + "ERROR_OPERATION_NOT_ALLOWED", + "ERROR_APP_NOT_AUTHORIZED", + "ERROR_UNAUTHORIZED_DOMAIN", + "ERROR_MISSING_CONTINUE_URI", + "ERROR_INVALID_CERT_HASH", + "ERROR_DYNAMIC_LINK_NOT_ACTIVATED", + "ERROR_INVALID_DYNAMIC_LINK_DOMAIN", + "ERROR_INVALID_HOSTING_LINK_DOMAIN", + "ERROR_INVALID_PROVIDER_ID", + "ERROR_ADMIN_RESTRICTED_OPERATION", + "ERROR_UNSUPPORTED_FIRST_FACTOR", + "ERROR_UNSUPPORTED_PASSTHROUGH_OPERATION", + "ERROR_INVALID_REQ_TYPE", + "ERROR_WEB_CONTEXT_ALREADY_PRESENTED", + "ERROR_INVALID_TENANT_ID", + "ERROR_TENANT_ID_MISMATCH", + "ERROR_UNSUPPORTED_TENANT_OPERATION", + "ERROR_RECAPTCHA_NOT_ENABLED", + "ERROR_CAPTCHA_CHECK_FAILED", + "ERROR_MISSING_RECAPTCHA_TOKEN", + "ERROR_INVALID_RECAPTCHA_TOKEN", + "ERROR_INVALID_RECAPTCHA_ACTION", + "ERROR_MISSING_RECAPTCHA_VERSION", + "ERROR_INVALID_RECAPTCHA_VERSION", + "ERROR_MISSING_CLIENT_TYPE", + "ERROR_MISSING_CLIENT_IDENTIFIER", + "ERROR_ALTERNATE_CLIENT_IDENTIFIER_REQUIRED", + // Email-template settings in the Firebase console. + "ERROR_INVALID_MESSAGE_PAYLOAD", + "ERROR_INVALID_SENDER", + "ERROR_INVALID_RECIPIENT_EMAIL", + // Host integration and project quota. The synthetic exception below pins the `when` + // arm; AuthExceptionRecoveryResolutionTest drives the SDK's own missing-activity type. + "ERROR_MISSING_ACTIVITY", + "ERROR_WEB_STORAGE_UNSUPPORTED", + "ERROR_QUOTA_EXCEEDED", + ) + // EmailAuthScreen and PhoneAuthScreen render `exception.message` inline without going + // through getRecoveryMessage, so the diagnostic must not be on the message at all. It + // stays on the cause, where logs and `exception.cause?.message` still reach it. + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorUnknownAuth).thenReturn("Generic unknown error") + whenever(stringProvider.unknownErrorRecoveryMessage).thenReturn("Une erreur est survenue") + + for (code in configurationCodes) { + val firebaseException = object : FirebaseAuthException(code, sdkText) {} + val result = AuthException.from(firebaseException, stringProvider) + assertWithMessage(code).that(result) + .isInstanceOf(AuthException.MisconfigurationException::class.java) + assertWithMessage(code).that(result.message).isEqualTo("Une erreur est survenue") + assertWithMessage(code).that(result.cause).isEqualTo(firebaseException) + assertWithMessage(code).that(result.cause?.message).isEqualTo(sdkText) + } + } + + @Test + fun `from() keeps internal errors out of MisconfigurationException`() { + // INTERNAL_ERROR is what the SDK falls back to for a status it does not recognise, and + // ERROR_WEB_INTERNAL_ERROR is a backend fault. Neither is a setup problem. + for (code in listOf("INTERNAL_ERROR", "ERROR_WEB_INTERNAL_ERROR")) { + val result = AuthException.from(object : FirebaseAuthException(code, sdkText) {}) + assertWithMessage(code).that(result) + .isInstanceOf(AuthException.UnknownException::class.java) + } + } + + @Test + fun `from() maps ERROR_USER_CANCELLED to AuthCancelledException`() { + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorAuthCancelled).thenReturn("You cancelled") + + for (code in listOf("ERROR_USER_CANCELLED", "ERROR_WEB_CONTEXT_CANCELED")) { + val result = AuthException.from(object : FirebaseAuthException(code, sdkText) {}, stringProvider) + assertWithMessage(code).that(result) + .isInstanceOf(AuthException.AuthCancelledException::class.java) + assertWithMessage(code).that(result.message).isEqualTo("You cancelled") + } + } + + @Test + fun `new AuthUIStringProvider members compile to real JVM default methods`() { + // Decision: every new member must be source- AND binary-compatible, so a host that + // implemented the interface before this change still compiles and still links. An + // abstract JVM method here would break every existing implementor at runtime. + val newMembers = listOf( + "getErrorIncorrectEmailOrPassword", + "getErrorInvalidVerificationId", + "getErrorRetryPhoneAuth", + "getErrorUserMismatch", + "getErrorPhoneNumberNotEnrolled", + "getErrorSessionExpired", + "getErrorMultiFactorSessionExpired", + "getErrorActionCodeInvalid", + "getErrorUnverifiedEmail", + "getErrorSecondFactorAlreadyEnrolled", + "getErrorMaximumSecondFactorCountExceeded", + ) + + for (name in newMembers) { + val method = AuthUIStringProvider::class.java.getMethod(name) + assertWithMessage(name).that(method.isDefault).isTrue() + } + } + @Test + fun `an expired user token honours the credentials hook, not the account-generic one`() { + // It produces an InvalidCredentialsException, so errorUserAccountGeneric — the hook the + // UserNotFoundException arm below it uses — must not win. + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.errorInvalidCredentials).thenReturn("Custom: credentials") + whenever(stringProvider.errorUserAccountGeneric).thenReturn("Custom: account generic") + val firebaseException = FirebaseAuthInvalidUserException("ERROR_USER_TOKEN_EXPIRED", "x") + + val result = AuthException.from(firebaseException, stringProvider) + + assertThat(result).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(result.message).isEqualTo("Custom: credentials") + } + } \ No newline at end of file diff --git a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt index 9bbbb16087..105a0d2a8b 100644 --- a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt @@ -41,6 +41,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.anyString +import org.mockito.Mockito.doAnswer import org.mockito.Mockito.doNothing import org.mockito.Mockito.doThrow import org.mockito.Mockito.mock @@ -436,7 +437,6 @@ class FirebaseAuthUITest { val mockUser = mock(FirebaseUser::class.java) val mockUserInfo = mock(UserInfo::class.java) `when`(mockUserInfo.providerId).thenReturn("google.com") - `when`(mockUser.providerId).thenReturn("google.com") `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) // Setup mock auth @@ -484,7 +484,6 @@ class FirebaseAuthUITest { val mockUser = mock(FirebaseUser::class.java) val mockUserInfo = mock(UserInfo::class.java) `when`(mockUserInfo.providerId).thenReturn("facebook.com") - `when`(mockUser.providerId).thenReturn("facebook.com") `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) // Setup mock auth @@ -524,7 +523,6 @@ class FirebaseAuthUITest { val mockUser = mock(FirebaseUser::class.java) val mockUserInfo = mock(UserInfo::class.java) `when`(mockUserInfo.providerId).thenReturn("password") - `when`(mockUser.providerId).thenReturn("password") `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) // Setup mock auth @@ -580,6 +578,87 @@ class FirebaseAuthUITest { verify(mockAuth).signOut() } + @Test + fun `signOut() calls Facebook sign out even though FirebaseAuth clears the user first`() = + runTest { + // Setup mock user with Facebook provider + val mockUser = mock(FirebaseUser::class.java) + val mockUserInfo = mock(UserInfo::class.java) + `when`(mockUserInfo.providerId).thenReturn("facebook.com") + `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) + + // Setup mock auth that clears currentUser on signOut(), like the real FirebaseAuth + val mockAuth = mock(FirebaseAuth::class.java) + var signedOut = false + `when`(mockAuth.currentUser).thenAnswer { if (signedOut) null else mockUser } + doAnswer { signedOut = true; null }.`when`(mockAuth).signOut() + + var facebookSignOutCalled = false + val mockLoginManagerProvider = object : AuthProvider.Facebook.LoginManagerProvider { + override fun getCredential(token: String): com.google.firebase.auth.AuthCredential { + throw UnsupportedOperationException("Not used in this test") + } + + override fun logOut() { + facebookSignOutCalled = true + } + } + + val instance = FirebaseAuthUI.create(defaultApp, mockAuth) + instance.testLoginManagerProvider = mockLoginManagerProvider + val context = ApplicationProvider.getApplicationContext() + + instance.signOut(context) + + assertThat(facebookSignOutCalled).isTrue() + assertThat(mockAuth.currentUser).isNull() + } + + @Test + fun `signOut() reads linked providers from providerData not FirebaseUser providerId`() = + runTest { + // The real FirebaseUser.providerId is always "firebase"; the per-provider ids live in + // providerData. + val mockUser = mock(FirebaseUser::class.java) + val mockUserInfo = mock(UserInfo::class.java) + `when`(mockUserInfo.providerId).thenReturn("google.com") + `when`(mockUser.providerId).thenReturn("firebase") + `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) + + val mockAuth = mock(FirebaseAuth::class.java) + `when`(mockAuth.currentUser).thenReturn(mockUser) + doNothing().`when`(mockAuth).signOut() + + var googleSignOutCalled = false + val mockCredentialManagerProvider = + object : AuthProvider.Google.CredentialManagerProvider { + override suspend fun getGoogleCredential( + context: Context, + credentialManager: androidx.credentials.CredentialManager, + serverClientId: String, + filterByAuthorizedAccounts: Boolean, + autoSelectEnabled: Boolean, + ): AuthProvider.Google.GoogleSignInResult { + throw UnsupportedOperationException("Not used in this test") + } + + override suspend fun clearCredentialState( + context: Context, + credentialManager: androidx.credentials.CredentialManager, + ) { + googleSignOutCalled = true + } + } + + val instance = FirebaseAuthUI.create(defaultApp, mockAuth) + instance.testCredentialManagerProvider = mockCredentialManagerProvider + val context = ApplicationProvider.getApplicationContext() + + instance.signOut(context) + + assertThat(googleSignOutCalled).isTrue() + } + // ============================================================================================= // Delete Account Tests // ============================================================================================= diff --git a/auth/src/test/java/com/firebase/ui/auth/PasswordPolicyMessageLocalizationTest.kt b/auth/src/test/java/com/firebase/ui/auth/PasswordPolicyMessageLocalizationTest.kt new file mode 100644 index 0000000000..7247fc6faf --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/PasswordPolicyMessageLocalizationTest.kt @@ -0,0 +1,311 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.ui.components.getRecoveryMessage +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.firebase.FirebaseException +import com.google.firebase.auth.FirebaseAuthWeakPasswordException +import java.util.Locale +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Covers the Google Identity Platform password-policy path. + * + * The fixtures are the real backend strings, captured from a project with `Require` enforcement + * and a minimum length of 10. + * + * @suppress Internal test class + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class PasswordPolicyMessageLocalizationTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + private val strings: AuthUIStringProvider = DefaultAuthUIStringProvider(context) + private val french: AuthUIStringProvider = DefaultAuthUIStringProvider(context, Locale.FRENCH) + + // ============================================================================================= + // The real backend strings + // ============================================================================================= + + private companion object { + /** Verbatim single-constraint probe responses. */ + const val MIN_LENGTH = "Password must contain at least 10 characters" + const val UPPER_CASE = "Password must contain an upper case character" + const val LOWER_CASE = "Password must contain a lower case character" + const val NUMERIC = "Password must contain a numeric character" + + /** + * Unverified: the probe project has special characters disabled. GIdP's wording is + * "non-alphanumeric", which is where the substring collision with [NUMERIC] comes from. + */ + const val NON_ALPHANUMERIC = "Password must contain a non-alphanumeric character" + + /** All failing requirements come back together, comma-separated, inside one bracket. */ + const val ALL_THREE = + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : Missing password requirements: " + + "[$MIN_LENGTH, $UPPER_CASE, $NUMERIC]" + + /** + * The marker with nothing parseable after it. `parsePasswordPolicyRequirements` returns an + * empty list, and the whole message is then the generic string. + */ + const val UNPARSEABLE = "PASSWORD_DOES_NOT_MEET_REQUIREMENTS" + + val ENGLISH_REQUIREMENT_SENTENCES = + listOf(MIN_LENGTH, UPPER_CASE, LOWER_CASE, NUMERIC) + } + + /** The two exception shapes that carry a policy rejection into `from()`. */ + private fun shapesFor(sourceText: String): List> = listOf( + // `from()` reads `reason` first for this type. + "FirebaseAuthWeakPasswordException" to + FirebaseAuthWeakPasswordException("ERROR_WEAK_PASSWORD", sourceText, sourceText), + "FirebaseException" to FirebaseException(sourceText), + ) + + // ============================================================================================= + // Untranslated English reaching the dialog + // ============================================================================================= + + @Test + fun `a parsed policy rejection never renders the backend's English sentences`() { + for ((shape, exception) in shapesFor(ALL_THREE)) { + val rendered = getRecoveryMessage(AuthException.from(exception, strings), strings) + + for (sentence in ENGLISH_REQUIREMENT_SENTENCES) { + assertWithMessage("%s -> dialog body still contains %s", shape, sentence) + .that(rendered).doesNotContain(sentence) + } + assertWithMessage("%s -> dialog body is blank", shape) + .that(rendered.isBlank()).isFalse() + } + } + + @Test + fun `an unparseable policy rejection never renders the old hardcoded literal`() { + for ((shape, exception) in shapesFor(UNPARSEABLE)) { + val rendered = getRecoveryMessage(AuthException.from(exception, strings), strings) + + // `errorWeakPasswordGeneric` is the host's hook and ships blank. + assertWithMessage("%s -> dialog body fell back to the hardcoded literal", shape) + .that(rendered).doesNotContain("Password does not meet policy requirements") + assertWithMessage("%s -> dialog body", shape) + .that(rendered).isEqualTo(strings.errorPasswordPolicyGeneric) + } + } + + @Test + fun `both policy paths resolve to French on a French device`() { + for (sourceText in listOf(ALL_THREE, UNPARSEABLE)) { + for ((shape, exception) in shapesFor(sourceText)) { + val rendered = getRecoveryMessage(AuthException.from(exception, french), french) + + for (sentence in ENGLISH_REQUIREMENT_SENTENCES) { + assertWithMessage("%s -> French dialog body contains %s", shape, sentence) + .that(rendered).doesNotContain(sentence) + } + assertWithMessage("%s -> French dialog body is not French", shape) + .that(rendered).contains("mot de passe") + } + } + } + + // ============================================================================================= + // What the mapping actually produces + // ============================================================================================= + + @Test + fun `each requirement maps to the library's own translated copy`() { + val rendered = getRecoveryMessage( + AuthException.from(FirebaseException(ALL_THREE), strings), strings + ) + + // The project's own minimum, read back out of the sentence — not the 6 that + // `weakPasswordRecoveryMessage` hardcodes. + assertThat(rendered).contains(strings.passwordTooShort(10)) + assertThat(rendered).contains(strings.passwordMissingUppercase) + assertThat(rendered).contains(strings.passwordMissingDigit) + // Only the three that failed. + assertThat(rendered).doesNotContain(strings.passwordMissingLowercase) + assertThat(rendered.lines()).hasSize(3) + } + + @Test + fun `the lower case requirement maps too`() { + // Not in ALL_THREE, so it needs its own fixture to be covered at all. + val rendered = getRecoveryMessage( + AuthException.from( + FirebaseException("PASSWORD_DOES_NOT_MEET_REQUIREMENTS: [$LOWER_CASE]"), strings + ), + strings + ) + + assertThat(rendered).isEqualTo(strings.passwordMissingLowercase) + } + + @Test + fun `the non-alphanumeric requirement maps to the special-character copy, not the digit one`() { + // "numeric" is a substring of "non-alphanumeric", so the digit arm can swallow this. + val rendered = getRecoveryMessage( + AuthException.from( + FirebaseException("PASSWORD_DOES_NOT_MEET_REQUIREMENTS: [$NON_ALPHANUMERIC]"), + strings + ), + strings + ) + + assertThat(rendered).isEqualTo(strings.passwordMissingSpecialCharacter) + assertThat(rendered).isNotEqualTo(strings.passwordMissingDigit) + } + + @Test + fun `the digit and special-character requirements stay distinct when both fail`() { + // The pair must resolve to two different strings whichever arm is tested first. + val rendered = getRecoveryMessage( + AuthException.from( + FirebaseException( + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS: [$NUMERIC, $NON_ALPHANUMERIC]" + ), + strings + ), + strings + ) + + assertThat(rendered.lines()).containsExactly( + strings.passwordMissingDigit, + strings.passwordMissingSpecialCharacter, + ).inOrder() + } + + @Test + fun `the spelled-out special character wording maps too`() { + val rendered = getRecoveryMessage( + AuthException.from( + FirebaseException( + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS: " + + "[Password must contain a special character]" + ), + strings + ), + strings + ) + + assertThat(rendered).isEqualTo(strings.passwordMissingSpecialCharacter) + } + + @Test + fun `an exclusive maximum-length requirement states the maximum, not the bound`() { + // "fewer than 4096" permits 4095, and passwordTooLong renders the maximum, not the bound. + val rendered = getRecoveryMessage( + AuthException.from( + FirebaseException( + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS: " + + "[Password must contain fewer than 4096 characters]" + ), + strings + ), + strings + ) + + assertThat(rendered).isEqualTo(strings.passwordTooLong(4095)) + assertThat(rendered).isNotEqualTo(strings.passwordTooLong(4096)) + } + + @Test + fun `an inclusive maximum-length requirement takes the number as written`() { + // "at most N" and "no more than N" are inclusive, so no adjustment applies. + for (wording in listOf("at most 64", "no more than 64")) { + val rendered = getRecoveryMessage( + AuthException.from( + FirebaseException( + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS: " + + "[Password must contain $wording characters]" + ), + strings + ), + strings + ) + + assertWithMessage(wording).that(rendered).isEqualTo(strings.passwordTooLong(64)) + } + } + + @Test + fun `an unrecognised requirement is kept verbatim rather than dropped`() { + val reworded = "Password must not be one of your last 5 passwords" + val rendered = getRecoveryMessage( + AuthException.from( + FirebaseException( + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS: [$UPPER_CASE, $reworded]" + ), + strings + ), + strings + ) + + // The known one is still translated... + assertThat(rendered).contains(strings.passwordMissingUppercase) + // ...and the unknown one is kept verbatim instead of vanishing or blanking the message. + assertThat(rendered).contains(reworded) + } + + @Test + fun `failingRequirements keeps the raw untranslated sentences`() { + val exception = AuthException.from(FirebaseException(ALL_THREE), french) + + assertThat(exception).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + val policy = exception as AuthException.PasswordPolicyViolationException + + // This list stays raw even when `message` is French. + assertThat(policy.failingRequirements) + .containsExactly(MIN_LENGTH, UPPER_CASE, NUMERIC).inOrder() + assertThat(policy.message).doesNotContain(MIN_LENGTH) + } + + @Test + fun `a policy rejection stays a PasswordPolicyViolationException, not a WeakPasswordException`() { + for ((shape, exception) in shapesFor(ALL_THREE)) { + assertWithMessage("%s", shape).that(AuthException.from(exception, strings)) + .isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + } + + // A plain weak-password rejection must not be pulled into the policy type. + val plain = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", "Password should be at least 6 characters", + "Password should be at least 6 characters" + ) + assertThat(AuthException.from(plain, strings)) + .isInstanceOf(AuthException.WeakPasswordException::class.java) + } + + @Test + fun `a null string provider still leaves the backend sentences readable`() { + // Nothing to resolve against, so the raw sentences are the only output. + val resolved = AuthException.from(FirebaseException(ALL_THREE), null as AuthUIStringProvider?) + + assertThat(resolved.message).contains(MIN_LENGTH) + assertThat(resolved.message).contains(UPPER_CASE) + } +} diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt index 937f698d0f..72ece3340d 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AnonymousAuthProviderFirebaseAuthUITest.kt @@ -23,6 +23,8 @@ import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.google.android.gms.tasks.TaskCompletionSource import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp @@ -422,4 +424,53 @@ class AnonymousAuthProviderFirebaseAuthUITest { ArgumentMatchers.anyString() ) } + + // ============================================================================================= + // Error message routing — the configured AuthUIStringProvider, not the device + // ============================================================================================= + + @Test + fun `signInAnonymously - failure message comes from the configured string provider`() = + runTest { + // "A network error has occurred", in Japanese. + val localizedMessage = "ネットワークエラーが発生しました" + val networkException = FirebaseNetworkException("A network error has occurred.") + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(networkException) + `when`(mockFirebaseAuth.signInAnonymously()).thenReturn(taskCompletionSource.task) + + val localizedConfig = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Anonymous) + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorNetworkGeneric: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(localizedConfig).signInAnonymously() + } catch (t: Throwable) { + thrown = t + } + + // Without the configured provider the conversion keeps Firebase's own English text. + assertThat(thrown).isInstanceOf(AuthException.NetworkException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt index 4cb63bc4f9..31058c2487 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProviderFirebaseAuthUITest.kt @@ -24,6 +24,9 @@ import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.configuration.PasswordRule import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.ui.components.getRecoveryMessage import com.firebase.ui.auth.util.EmailLinkPersistenceManager import com.firebase.ui.auth.util.MockPersistenceManager import com.google.android.gms.tasks.TaskCompletionSource @@ -31,13 +34,16 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions +import com.google.firebase.FirebaseTooManyRequestsException import com.google.firebase.auth.ActionCodeSettings import com.google.firebase.auth.AuthCredential import com.google.firebase.auth.AuthResult import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthException import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.FirebaseAuthInvalidUserException import com.google.firebase.auth.FirebaseAuthUserCollisionException +import com.google.firebase.auth.FirebaseAuthWeakPasswordException import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider import com.google.firebase.auth.SignInMethodQueryResult @@ -2030,4 +2036,498 @@ class EmailAuthProviderFirebaseAuthUITest { val state = instance.authStateFlow().first { it !is AuthState.Loading } assertThat(state).isEqualTo(AuthState.Success(result = mockAuthResult, user = mockUser, isNewUser = false)) } + + // ============================================================================================= + // Error message routing — the configured AuthUIStringProvider, not the device + // + // Every test here overrides exactly one member of the provider and asserts that string comes + // back on both the thrown exception and the emitted AuthState.Error. Reverting the call site + // to `AuthException.from(e)` or `AuthException.from(e, context)` leaves Firebase's own English + // text in place and fails the test. + // ============================================================================================= + + /** A provider whose only difference from the default is [errorWeakPasswordGeneric]. */ + private fun weakPasswordProvider(message: String): AuthUIStringProvider = + object : AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorWeakPasswordGeneric: String = message + } + + @Test + fun `createOrLinkUserWithEmailAndPassword - weak password message comes from the configured string provider`() = + runTest { + // "The password is too weak", in Japanese. + val localizedMessage = "パスワードが弱すぎます" + val weakPasswordException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + "The given password is invalid.", + "Password should be at least 6 characters" + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(weakPasswordException) + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + `when`( + mockFirebaseAuth.createUserWithEmailAndPassword( + "test@example.com", + "Pass@123" + ) + ).thenReturn(taskCompletionSource.task) + + val emailProvider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(emailProvider) } + stringProvider = weakPasswordProvider(localizedMessage) + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).createOrLinkUserWithEmailAndPassword( + context = applicationContext, + provider = emailProvider, + name = null, + email = "test@example.com", + password = "Pass@123" + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.WeakPasswordException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `createOrLinkUserWithEmailAndPassword - policy violation with no listed requirements uses the configured string provider`() = + runTest { + // "The password does not meet the requirements", in Japanese. + val localizedMessage = "パスワードが要件を満たしていません" + // No bracketed requirement list, so the message has to come from the provider. + val policyException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + "The given password is invalid.", + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS" + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(policyException) + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + `when`( + mockFirebaseAuth.createUserWithEmailAndPassword( + "test@example.com", + "Pass@123" + ) + ).thenReturn(taskCompletionSource.task) + + val emailProvider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(emailProvider) } + stringProvider = weakPasswordProvider(localizedMessage) + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).createOrLinkUserWithEmailAndPassword( + context = applicationContext, + provider = emailProvider, + name = null, + email = "test@example.com", + password = "Pass@123" + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown) + .isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `signInWithEmailAndPassword - user-not-found message comes from the configured string provider`() = + runTest { + // "No account was found for that email address", in Japanese. + val localizedMessage = "そのメールアドレスのアカウントは見つかりませんでした" + val userNotFoundException = FirebaseAuthInvalidUserException( + "ERROR_USER_NOT_FOUND", + "User not found" + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(userNotFoundException) + `when`(mockFirebaseAuth.signInWithEmailAndPassword("test@example.com", "Pass@123")) + .thenReturn(taskCompletionSource.task) + + val config = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorUserNotFound: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).signInWithEmailAndPassword( + context = applicationContext, + email = "test@example.com", + password = "Pass@123" + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.UserNotFoundException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `signInAndLinkWithCredential - failure message comes from the configured string provider`() = + runTest { + // "Those credentials are not valid", in Japanese. + val localizedMessage = "その認証情報は有効ではありません" + val credential = GoogleAuthProvider.getCredential("google-id-token", null) + val invalidCredentialsException = FirebaseAuthInvalidCredentialsException( + "ERROR_INVALID_CREDENTIAL", + "Invalid credential" + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(invalidCredentialsException) + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + `when`(mockFirebaseAuth.signInWithCredential(credential)) + .thenReturn(taskCompletionSource.task) + + val config = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorInvalidCredentials: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).signInAndLinkWithCredential(credential) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `sendSignInLinkToEmail - failure message comes from the configured string provider`() = + runTest { + // "Too many attempts. Please try again later", in Japanese. + val localizedMessage = "試行回数が多すぎます。しばらくしてからもう一度お試しください" + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + // Rate limiting is a FirebaseTooManyRequestsException, not a FirebaseAuthException: + // it is not an auth exception at all and carries no error code. + val tooManyRequests = FirebaseTooManyRequestsException( + "We have blocked all requests from this device due to unusual activity." + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(tooManyRequests) + `when`(mockFirebaseAuth.sendSignInLinkToEmail(anyString(), any())) + .thenReturn(taskCompletionSource.task) + + val provider = AuthProvider.Email( + isEmailLinkSignInEnabled = true, + emailLinkActionCodeSettings = ActionCodeSettings.newBuilder() + .setUrl("https://example.com") + .setHandleCodeInApp(true) + .setAndroidPackageName("com.test", true, null) + .build(), + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(provider) } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorTooManyRequests: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).sendSignInLinkToEmail( + context = applicationContext, + provider = provider, + email = "test@example.com", + credentialForLinking = null + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.TooManyRequestsException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `sendPasswordResetEmail - failure message comes from the configured string provider`() = + runTest { + // "This account has been disabled", in Japanese. + val localizedMessage = "このアカウントは無効になっています" + val disabledException = FirebaseAuthInvalidUserException( + "ERROR_USER_DISABLED", + "The user account has been disabled by an administrator." + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(disabledException) + `when`( + mockFirebaseAuth.sendPasswordResetEmail( + ArgumentMatchers.eq("test@example.com"), + ArgumentMatchers.isNull() + ) + ).thenReturn(taskCompletionSource.task) + + val config = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorUserDisabled: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).sendPasswordResetEmail("test@example.com") + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `signInWithEmailAndPassword - wrong password uses the per-code string, not the invalid-credentials one`() = + runTest { + // "The password is incorrect", in Japanese. ERROR_WRONG_PASSWORD used to share one flat + // arm with every other invalid-credential code, so this string was unreachable. + val localizedMessage = "パスワードが正しくありません" + val wrongPassword = FirebaseAuthInvalidCredentialsException( + "ERROR_WRONG_PASSWORD", + "The password is invalid or the user does not have a password. [ INVALID_PASSWORD ]" + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(wrongPassword) + `when`(mockFirebaseAuth.signInWithEmailAndPassword("test@example.com", "Pass@123")) + .thenReturn(taskCompletionSource.task) + + val config = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val invalidPassword: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).signInWithEmailAndPassword( + context = applicationContext, + email = "test@example.com", + password = "Pass@123" + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `signInWithEmailAndPassword - merged invalid-credential code does not blame the password`() = + runTest { + // With email enumeration protection on, a wrong password and a nonexistent account both + // arrive as ERROR_INVALID_CREDENTIAL, so "Incorrect password" would be a false claim. + val incorrectEmailOrPassword = "That email or password isn't correct" + val invalidCredential = FirebaseAuthInvalidCredentialsException( + "ERROR_INVALID_CREDENTIAL", + "The supplied auth credential is incorrect, malformed or has expired." + ) + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(invalidCredential) + `when`(mockFirebaseAuth.signInWithEmailAndPassword("test@example.com", "Pass@123")) + .thenReturn(taskCompletionSource.task) + + val config = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).signInWithEmailAndPassword( + context = applicationContext, + email = "test@example.com", + password = "Pass@123" + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(incorrectEmailOrPassword) + assertThat(thrown).hasMessageThat() + .isNotEqualTo(applicationContext.getString(R.string.fui_error_invalid_password)) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(incorrectEmailOrPassword) + } + + @Test + fun `sendSignInLinkToEmail - a disabled provider surfaces as MisconfigurationException`() = + runTest { + // The user can do nothing about a provider left disabled in the Firebase console, so + // the raw diagnostic stays on the cause for logs and never reaches the message. + val rawDiagnostic = "This operation is not allowed. This may be because the given " + + "sign-in provider is disabled for this Firebase project. [ OPERATION_NOT_ALLOWED ]" + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + val notAllowed = + object : FirebaseAuthException("ERROR_OPERATION_NOT_ALLOWED", rawDiagnostic) {} + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException(notAllowed) + `when`(mockFirebaseAuth.sendSignInLinkToEmail(anyString(), any())) + .thenReturn(taskCompletionSource.task) + + val provider = AuthProvider.Email( + isEmailLinkSignInEnabled = true, + emailLinkActionCodeSettings = ActionCodeSettings.newBuilder() + .setUrl("https://example.com") + .setHandleCodeInApp(true) + .setAndroidPackageName("com.test", true, null) + .build(), + passwordValidationRules = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(provider) } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).sendSignInLinkToEmail( + context = applicationContext, + provider = provider, + email = "test@example.com", + credentialForLinking = null + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.MisconfigurationException::class.java) + // EmailAuthScreen renders `exception.message` inline, so the message itself has to be + // clean; the diagnostic is still reachable as `exception.cause?.message`. + assertThat(thrown).hasMessageThat().doesNotContain("Firebase") + assertThat(thrown).hasMessageThat().doesNotContain("OPERATION_NOT_ALLOWED") + assertThat(thrown?.cause).isEqualTo(notAllowed) + assertThat(thrown?.cause).hasMessageThat().isEqualTo(rawDiagnostic) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + val emitted = (state as AuthState.Error).exception + assertThat(emitted).isInstanceOf(AuthException.MisconfigurationException::class.java) + assertThat( + getRecoveryMessage( + emitted as AuthException, + DefaultAuthUIStringProvider(applicationContext) + ) + ).doesNotContain("Firebase") + } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt index db2931f03d..1b41464b9a 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt @@ -27,10 +27,13 @@ import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.authUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider.Facebook.FacebookProfileData +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.util.EmailLinkPersistenceManager import com.google.android.gms.tasks.TaskCompletionSource import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseNetworkException import com.google.firebase.FirebaseOptions import com.google.firebase.auth.AuthCredential import com.google.firebase.auth.AuthResult @@ -356,4 +359,100 @@ class FacebookAuthProviderFirebaseAuthUITest { assertThat(e).isInstanceOf(AuthException.UnknownException::class.java) } } + + // ============================================================================================= + // Error message routing — the configured AuthUIStringProvider, not the device + // ============================================================================================= + + @Test + @Config(manifest = Config.NONE, qualifiers = "night") + fun `signInWithFacebook - FacebookException message comes from the configured string provider`() = + runTest { + // "An unknown error occurred during sign-in", in Japanese. + val localizedMessage = "サインイン中に不明なエラーが発生しました" + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val provider = spy(AuthProvider.Facebook()) + val config = authUIConfiguration { + context = applicationContext + providers { provider(provider) } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorUnknownAuth: String = localizedMessage + } + } + + val mockAccessToken = mock { + on { token } doReturn "error-token" + } + doAnswer { + throw FacebookException("Graph error") + }.whenever(provider).fetchFacebookProfile(any()) + + var thrown: Throwable? = null + try { + instance.flowScope(config).signInWithFacebook( + context = applicationContext, + provider = provider, + accessToken = mockAccessToken, + credentialProvider = mockFBAuthCredentialProvider + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.UnknownException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first { it is AuthState.Error } + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + @Config(manifest = Config.NONE, qualifiers = "night") + fun `signInWithFacebook - credential failure message comes from the configured string provider`() = + runTest { + // "A network error has occurred", in Japanese. + val localizedMessage = "ネットワークエラーが発生しました" + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val provider = spy(AuthProvider.Facebook()) + val config = authUIConfiguration { + context = applicationContext + providers { provider(provider) } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorNetworkGeneric: String = localizedMessage + } + } + + val mockAccessToken = mock { + on { token } doReturn "network-token" + } + doReturn(null).whenever(provider).fetchFacebookProfile(any()) + // A FirebaseException that is not a FirebaseAuthException, so it maps to + // NetworkException. Raised from the token exchange, which sits in signInWithFacebook's + // own body rather than in the delegated signInAndLinkWithCredential. + doAnswer { + throw FirebaseNetworkException("A network error has occurred.") + }.whenever(mockFBAuthCredentialProvider).getCredential("network-token") + + var thrown: Throwable? = null + try { + instance.flowScope(config).signInWithFacebook( + context = applicationContext, + provider = provider, + accessToken = mockAccessToken, + credentialProvider = mockFBAuthCredentialProvider + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.NetworkException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first { it is AuthState.Error } + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt index 8aaffe0040..a8a3238786 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt @@ -27,6 +27,9 @@ import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.recordingScope import com.google.android.gms.common.api.Scope import com.google.android.gms.tasks.TaskCompletionSource import com.google.common.truth.Truth.assertThat @@ -1115,4 +1118,112 @@ class GoogleAuthProviderFirebaseAuthUITest { assertThat(reportedFailures).isEmpty() } + + // ============================================================================================= + // Error message routing — the configured AuthUIStringProvider, not the device + // ============================================================================================= + + @Test + fun `signInWithGoogle - credential manager failure message comes from the configured string provider`() = + runTest { + // "An unknown error occurred during sign-in", in Japanese. + val localizedMessage = "サインイン中に不明なエラーが発生しました" + `when`( + mockCredentialManagerProvider.getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(true), + autoSelectEnabled = eq(false) + ) + ).thenThrow(RuntimeException("No credentials available")) + + val googleProvider = AuthProvider.Google( + serverClientId = "test-client-id", + scopes = emptyList() + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(googleProvider) } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorUnknownAuth: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).signInWithGoogle( + context = applicationContext, + provider = googleProvider, + authorizationProvider = mockAuthorizationProvider, + credentialManagerProvider = mockCredentialManagerProvider + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.UnknownException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + + @Test + fun `signInWithGoogle - scope authorization failure message comes from the configured string provider`() = + runTest { + // "An unknown error occurred during sign-in", in Japanese. + val localizedMessage = "サインイン中に不明なエラーが発生しました" + `when`(mockAuthorizationProvider.authorize(eq(applicationContext), any())) + .thenThrow(RuntimeException("Authorization failed")) + // Sign-in continues past the authorization failure, so the Error state is transient: + // a recording scope keeps it instead of letting the later states overwrite it. + `when`( + mockCredentialManagerProvider.getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(true), + autoSelectEnabled = eq(false) + ) + ).thenAnswer { throw AuthException.AuthCancelledException("stop here") } + + val googleProvider = AuthProvider.Google( + serverClientId = "test-client-id", + scopes = listOf("https://www.googleapis.com/auth/drive") + ) + val config = authUIConfiguration { + context = applicationContext + providers { provider(googleProvider) } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorUnknownAuth: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val recorded = mutableListOf() + try { + instance.recordingScope(config, recorded).signInWithGoogle( + context = applicationContext, + provider = googleProvider, + authorizationProvider = mockAuthorizationProvider, + credentialManagerProvider = mockCredentialManagerProvider + ) + } catch (_: Throwable) { + // The cancellation that stops the flow after the authorization failure. + } + + verify(mockAuthorizationProvider).authorize(eq(applicationContext), any()) + val authorizationError = recorded + .filterIsInstance() + .firstOrNull { it.exception is AuthException.UnknownException } + assertThat(authorizationError).isNotNull() + assertThat(authorizationError!!.exception).hasMessageThat() + .isEqualTo(localizedMessage) + } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt index 644324d47b..72fbbe5e89 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt @@ -23,6 +23,8 @@ import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.TaskCompletionSource import com.google.common.truth.Truth.assertThat @@ -32,6 +34,7 @@ import com.google.firebase.FirebaseOptions import com.google.firebase.auth.AuthCredential import com.google.firebase.auth.AuthResult import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.FirebaseAuthUserCollisionException import com.google.firebase.auth.FirebaseAuthWebException import com.google.firebase.auth.FirebaseUser @@ -444,4 +447,60 @@ class OAuthProviderFirebaseAuthUITest { assertThat(reportedFailures).isEmpty() } + + // ============================================================================================= + // Error message routing — the configured AuthUIStringProvider, not the device + // ============================================================================================= + + @Test + fun `signInWithProvider - failure message comes from the configured string provider`() = + runTest { + // "Those credentials are not valid", in Japanese. + val localizedMessage = "その認証情報は有効ではありません" + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setException( + FirebaseAuthInvalidCredentialsException( + "ERROR_INVALID_CREDENTIAL", + "The supplied auth credential is malformed or has expired." + ) + ) + `when`(mockFirebaseAuth.pendingAuthResult).thenReturn(null) + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + `when`( + mockFirebaseAuth.startActivityForSignInWithProvider( + any(), + any() + ) + ).thenReturn(taskCompletionSource.task) + + val githubProvider = AuthProvider.Github(customParameters = emptyMap()) + val config = authUIConfiguration { + context = applicationContext + providers { provider(githubProvider) } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorInvalidCredentials: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(config).signInWithProvider( + applicationContext, + activity = mockActivity, + provider = githubProvider + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt index 11f87f249a..efcd3e6301 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt @@ -23,12 +23,15 @@ import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.google.android.gms.tasks.TaskCompletionSource import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.auth.AuthResult import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.MultiFactorSession import com.google.firebase.auth.PhoneAuthCredential @@ -406,6 +409,75 @@ class PhoneAuthProviderFirebaseAuthUITest { .isNotInstanceOf(AuthState.Error::class.java) } + @Test + fun `verifyPhoneNumber - failure message comes from the configured string provider`() = + runTest { + // "The format of the phone number is incorrect", in Japanese. + val localizedMessage = "電話番号の形式が正しくありません" + val localizedConfig = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null, + ) + ) + } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorInvalidCredentials: String = localizedMessage + } + } + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val phoneProvider = AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null, + timeout = 60L, + ) + val rejectingVerifier = object : AuthProvider.Phone.Verifier { + override fun verifyPhoneNumber( + auth: FirebaseAuth, + activity: Activity?, + phoneNumber: String, + timeout: Long, + forceResendingToken: PhoneAuthProvider.ForceResendingToken?, + multiFactorSession: MultiFactorSession?, + isInstantVerificationEnabled: Boolean, + ): Flow = flow { + throw FirebaseAuthInvalidCredentialsException( + "ERROR_INVALID_PHONE_NUMBER", + "The format of the phone number provided is incorrect." + ) + } + } + + var thrown: Throwable? = null + try { + instance.flowScope(localizedConfig).verifyPhoneNumber( + provider = phoneProvider, + activity = null, + phoneNumber = "not-a-number", + verifier = rejectingVerifier + ) + } catch (t: Throwable) { + thrown = t + } + + // Building the exception without the configured provider leaves Firebase's own English + // message on it, and the error dialog renders that verbatim. + assertThat(thrown).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(thrown).hasMessageThat() + .isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } + @Test fun `verifyPhoneNumber - cancellation does not clobber a newer unrelated state`() = runTest { val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) @@ -607,4 +679,55 @@ class PhoneAuthProviderFirebaseAuthUITest { verify(anonymousUser).linkWithCredential(mockCredential) } + @Test + fun `submitVerificationCode - failure message comes from the configured string provider`() = + runTest { + // "The verification code is incorrect", in Japanese. + val localizedMessage = "確認コードが正しくありません" + // Raised while building the credential, which is submitVerificationCode's own work: + // everything after it is delegated to signInAndLinkWithCredential. + `when`(mockPhoneAuthCredentialProvider.getCredential("test-verification-id", "000000")) + .thenAnswer { + throw FirebaseAuthInvalidCredentialsException( + "ERROR_INVALID_VERIFICATION_CODE", + "The sms verification code used to create the phone auth credential is invalid." + ) + } + + val phoneProvider = AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null, + timeout = 60L, + ) + val localizedConfig = authUIConfiguration { + context = applicationContext + providers { provider(phoneProvider) } + stringProvider = object : + AuthUIStringProvider by DefaultAuthUIStringProvider(applicationContext) { + override val errorInvalidCredentials: String = localizedMessage + } + } + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + var thrown: Throwable? = null + try { + instance.flowScope(localizedConfig).submitVerificationCode( + applicationContext, + verificationId = "test-verification-id", + code = "000000", + credentialProvider = mockPhoneAuthCredentialProvider + ) + } catch (t: Throwable) { + thrown = t + } + + assertThat(thrown).isInstanceOf(AuthException.InvalidCredentialsException::class.java) + assertThat(thrown).hasMessageThat().isEqualTo(localizedMessage) + + val state = instance.authStateFlow().first() + assertThat(state).isInstanceOf(AuthState.Error::class.java) + assertThat((state as AuthState.Error).exception).hasMessageThat() + .isEqualTo(localizedMessage) + } } \ No newline at end of file diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt index c095131a13..f4c373d07e 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt @@ -4,6 +4,7 @@ import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider import com.google.common.truth.Truth import com.google.firebase.auth.EmailAuthProvider +import com.google.firebase.auth.FirebaseAuthException import com.google.firebase.auth.GoogleAuthProvider import org.junit.Test import org.junit.runner.RunWith @@ -41,9 +42,22 @@ class ErrorRecoveryDialogLogicTest { // ============================================================================================= @Test - fun `getRecoveryMessage returns network error message for NetworkException`() { + fun `getRecoveryMessage prefers the library-owned message for NetworkException`() { + // Arrange - AuthException.from now puts the configured provider's string on the exception, + // so discarding it here would throw away the host's own translated copy. + val error = AuthException.NetworkException("Pas de connexion Internet") + + // Act + val message = getRecoveryMessage(error, mockStringProvider) + + // Assert + Truth.assertThat(message).isEqualTo("Pas de connexion Internet") + } + + @Test + fun `getRecoveryMessage returns network error message for NetworkException with blank message`() { // Arrange - val error = AuthException.NetworkException("Network error") + val error = AuthException.NetworkException("") // Act val message = getRecoveryMessage(error, mockStringProvider) @@ -77,15 +91,16 @@ class ErrorRecoveryDialogLogicTest { } @Test - fun `getRecoveryMessage returns generic message for InvalidCredentialsException with generic error text`() { - // Arrange - When error message is the generic fallback + fun `getRecoveryMessage shows the hardcoded fallback text for InvalidCredentialsException`() { + // Arrange - The old sentinel dropped this exact string on the floor. It could never match + // real traffic anyway: the SDK formats every message as " [ ]". val error = AuthException.InvalidCredentialsException("Invalid credentials provided") // Act val message = getRecoveryMessage(error, mockStringProvider) - // Assert - Should show the localized generic message - Truth.assertThat(message).isEqualTo("Incorrect password.") + // Assert + Truth.assertThat(message).isEqualTo("Invalid credentials provided") } @Test @@ -101,22 +116,34 @@ class ErrorRecoveryDialogLogicTest { } @Test - fun `getRecoveryMessage returns user not found message for UserNotFoundException`() { + fun `getRecoveryMessage prefers the library-owned message for UserNotFoundException`() { // Arrange - val error = AuthException.UserNotFoundException("User not found") + val error = AuthException.UserNotFoundException("Aucun compte pour cette adresse") // Act val message = getRecoveryMessage(error, mockStringProvider) // Assert - Truth.assertThat(message).isEqualTo("That email address doesn't match an existing account") + Truth.assertThat(message).isEqualTo("Aucun compte pour cette adresse") } @Test - fun `getRecoveryMessage returns weak password message with reason for WeakPasswordException`() { + fun `getRecoveryMessage returns user not found message for UserNotFoundException with blank message`() { // Arrange + val error = AuthException.UserNotFoundException("") + + // Act + val message = getRecoveryMessage(error, mockStringProvider) + + // Assert + Truth.assertThat(message).isEqualTo("That email address doesn't match an existing account") + } + + @Test + fun `getRecoveryMessage drops the untranslated reason for WeakPasswordException`() { + // Arrange - the reason is the raw SDK string, English in every locale. val error = AuthException.WeakPasswordException( - "Password is too weak", + "", null, "Password should be at least 8 characters" ) @@ -124,14 +151,17 @@ class ErrorRecoveryDialogLogicTest { // Act val message = getRecoveryMessage(error, mockStringProvider) - // Assert - Truth.assertThat(message).isEqualTo("Password not strong enough. Use at least 6 characters and a mix of letters and numbers\n\nReason: Password should be at least 8 characters") + // Assert - blank message, so the provider string supplies the whole body. The reason is + // not appended: it is untranslated, and the provider string already states the minimum. + Truth.assertThat(message).isEqualTo("Password not strong enough. Use at least 6 characters and a mix of letters and numbers") + Truth.assertThat(message).doesNotContain("Reason:") + Truth.assertThat(message).doesNotContain("Password should be at least 8 characters") } @Test fun `getRecoveryMessage returns weak password message without reason for WeakPasswordException`() { // Arrange - val error = AuthException.WeakPasswordException("Password is too weak", null, null) + val error = AuthException.WeakPasswordException("", null, null) // Act val message = getRecoveryMessage(error, mockStringProvider) @@ -144,7 +174,7 @@ class ErrorRecoveryDialogLogicTest { fun `getRecoveryMessage returns email already in use message with email for EmailAlreadyInUseException`() { // Arrange val error = AuthException.EmailAlreadyInUseException( - "Email already in use", + "", null, "test@example.com" ) @@ -152,14 +182,14 @@ class ErrorRecoveryDialogLogicTest { // Act val message = getRecoveryMessage(error, mockStringProvider) - // Assert + // Assert - blank message, so the provider string supplies the base and the email is kept Truth.assertThat(message).isEqualTo("Email account registration unsuccessful (test@example.com)") } @Test fun `getRecoveryMessage returns email already in use message without email for EmailAlreadyInUseException`() { // Arrange - val error = AuthException.EmailAlreadyInUseException("Email already in use", null, null) + val error = AuthException.EmailAlreadyInUseException("", null, null) // Act val message = getRecoveryMessage(error, mockStringProvider) @@ -168,6 +198,103 @@ class ErrorRecoveryDialogLogicTest { Truth.assertThat(message).isEqualTo("Email account registration unsuccessful") } + // ============================================================================================= + // Misconfiguration — the one message that is never rendered + // ============================================================================================= + + @Test + fun `getRecoveryMessage never shows the raw message for MisconfigurationException`() { + // Arrange - exactly what firebase-auth 24.2.0 puts on a disabled sign-in provider. It is + // untranslated, it names the Firebase console, and the user can do nothing with it. + val rawDiagnostic = "This operation is not allowed. This may be because the given sign-in " + + "provider is disabled for this Firebase project. Enable it in the Firebase " + + "console, under the sign-in method tab of the Auth section. [ OPERATION_NOT_ALLOWED ]" + val error = AuthException.MisconfigurationException(rawDiagnostic) + + // Act + val message = getRecoveryMessage(error, mockStringProvider) + + // Assert + Truth.assertThat(message).isEqualTo("An unknown error occurred.") + Truth.assertThat(message).doesNotContain("Firebase") + Truth.assertThat(message).doesNotContain("OPERATION_NOT_ALLOWED") + } + + @Test + fun `MisconfigurationException from() keeps the raw diagnostic on the cause, not the message`() { + // EmailAuthScreen and PhoneAuthScreen render `exception.message` inline without going + // through getRecoveryMessage, so the diagnostic has to be off the message entirely. + val rawDiagnostic = "The supplied auth credential is malformed. [ INVALID_CERT_HASH ]" + val firebaseException = + object : FirebaseAuthException("ERROR_INVALID_CERT_HASH", rawDiagnostic) {} + + val error = AuthException.from(firebaseException, mockStringProvider) + + Truth.assertThat(error).isInstanceOf(AuthException.MisconfigurationException::class.java) + Truth.assertThat(error.message).isEqualTo("An unknown error occurred.") + Truth.assertThat(error.cause).isEqualTo(firebaseException) + Truth.assertThat(error.cause?.message).isEqualTo(rawDiagnostic) + } + + @Test + fun `isRecoverable returns false for MisconfigurationException`() { + val error = AuthException.MisconfigurationException("Unauthorized domain") + + Truth.assertThat(isRecoverable(error)).isFalse() + } + + // ============================================================================================= + // Subtypes whose arms used to discard error.message outright + // ============================================================================================= + + @Test + fun `getRecoveryMessage prefers the library-owned message for TooManyRequestsException`() { + val error = AuthException.TooManyRequestsException("Trop de tentatives") + + Truth.assertThat(getRecoveryMessage(error, mockStringProvider)) + .isEqualTo("Trop de tentatives") + } + + @Test + fun `getRecoveryMessage returns the recovery string for TooManyRequestsException with blank message`() { + val error = AuthException.TooManyRequestsException("") + + Truth.assertThat(getRecoveryMessage(error, mockStringProvider)) + .isEqualTo("This phone number has been used too many times") + } + + @Test + fun `getRecoveryMessage prefers the library-owned message for MfaRequiredException`() { + val error = AuthException.MfaRequiredException("Vérification supplémentaire requise") + + Truth.assertThat(getRecoveryMessage(error, mockStringProvider)) + .isEqualTo("Vérification supplémentaire requise") + } + + @Test + fun `getRecoveryMessage returns the recovery string for MfaRequiredException with blank message`() { + val error = AuthException.MfaRequiredException("") + + Truth.assertThat(getRecoveryMessage(error, mockStringProvider)) + .isEqualTo("Additional verification required. Please complete multi-factor authentication.") + } + + @Test + fun `getRecoveryMessage prefers the library-owned message for AuthCancelledException`() { + val error = AuthException.AuthCancelledException("Connexion annulée") + + Truth.assertThat(getRecoveryMessage(error, mockStringProvider)) + .isEqualTo("Connexion annulée") + } + + @Test + fun `getRecoveryMessage returns the recovery string for AuthCancelledException with blank message`() { + val error = AuthException.AuthCancelledException("") + + Truth.assertThat(getRecoveryMessage(error, mockStringProvider)) + .isEqualTo("Authentication was cancelled. Please try again when ready.") + } + // ============================================================================================= // Recovery Action Text Tests // ============================================================================================= diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/components/TopLevelDialogControllerTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/components/TopLevelDialogControllerTest.kt index 60fa0ba406..902cade0bb 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/components/TopLevelDialogControllerTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/components/TopLevelDialogControllerTest.kt @@ -1,5 +1,11 @@ package com.firebase.ui.auth.ui.components +import android.content.Context +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -7,6 +13,7 @@ import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -39,8 +46,10 @@ class TopLevelDialogControllerTest { lateinit var controller: TopLevelDialogController composeTestRule.setContent { - controller = rememberTopLevelDialogController(stringProvider) { state } - controller.CurrentDialog() + CompositionLocalProvider(LocalAuthUIStringProvider provides stringProvider) { + controller = rememberTopLevelDialogController { state } + controller.CurrentDialog() + } } val error = AuthState.Error(Exception("boom")) @@ -69,8 +78,10 @@ class TopLevelDialogControllerTest { lateinit var controller: TopLevelDialogController composeTestRule.setContent { - controller = rememberTopLevelDialogController(stringProvider) { state } - controller.CurrentDialog() + CompositionLocalProvider(LocalAuthUIStringProvider provides stringProvider) { + controller = rememberTopLevelDialogController { state } + controller.CurrentDialog() + } } val error = AuthState.Error(Exception("boom")) @@ -97,6 +108,53 @@ class TopLevelDialogControllerTest { composeTestRule.onNodeWithText(stringProvider.errorDialogTitle).assertDoesNotExist() } + @Test + fun `de-dup fallback reads the latest authState lambda, not the first composition's`() { + stringProvider = DefaultAuthUIStringProvider(ApplicationProvider.getApplicationContext()) + val liveState = mutableStateOf(AuthState.Idle) + lateinit var controller: TopLevelDialogController + + composeTestRule.setContent { + // Mirrors FirebaseAuthScreen, which reads the collected state into a local `val` and + // passes `{ authState }`: every recomposition hands the factory a *new* lambda that + // has captured that frame's value, so an unkeyed `remember` would pin the first one. + val authState = liveState.value + CompositionLocalProvider(LocalAuthUIStringProvider provides stringProvider) { + controller = rememberTopLevelDialogController { authState } + controller.CurrentDialog() + } + } + + val error = AuthState.Error(Exception("boom")) + val exception = AuthException.from(error.exception, stringProvider) + + // Recompose with the Error before showing anything, so the first composition's captured + // value (Idle) and the live one differ. + composeTestRule.runOnIdle { liveState.value = error } + composeTestRule.waitForIdle() + + // No errorState argument, so `currentAuthState()` is the only path that can record the + // Error for de-duplication. + composeTestRule.runOnIdle { + controller.showErrorDialog(exception = exception) + } + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText(stringProvider.errorDialogTitle).assertExists() + + composeTestRule.runOnIdle { controller.dismissDialog() } + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText(stringProvider.errorDialogTitle).assertDoesNotExist() + + // Same Error still live, same exception instance: the fallback must have recorded it, so + // this repeat is a no-op. With a pinned first-composition lambda the fallback resolves to + // Idle, records nothing, and the dialog comes back. + composeTestRule.runOnIdle { + controller.showErrorDialog(exception = exception) + } + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText(stringProvider.errorDialogTitle).assertDoesNotExist() + } + @Test fun `second observer of the same error does not overwrite the first observer's dialog`() { stringProvider = DefaultAuthUIStringProvider(ApplicationProvider.getApplicationContext()) @@ -104,8 +162,10 @@ class TopLevelDialogControllerTest { lateinit var controller: TopLevelDialogController composeTestRule.setContent { - controller = rememberTopLevelDialogController(stringProvider) { state } - controller.CurrentDialog() + CompositionLocalProvider(LocalAuthUIStringProvider provides stringProvider) { + controller = rememberTopLevelDialogController { state } + controller.CurrentDialog() + } } val error = AuthState.Error(Exception("boom")) @@ -150,4 +210,66 @@ class TopLevelDialogControllerTest { "secondOnRetryCalled=$secondOnRetryCalled)" } } + + @Test + fun `dialog survives a host that rebuilds its string provider every recomposition`() { + val context = ApplicationProvider.getApplicationContext() + stringProvider = DefaultAuthUIStringProvider(context) + var state: AuthState = AuthState.Idle + lateinit var controller: TopLevelDialogController + var tick by mutableIntStateOf(0) + + composeTestRule.setContent { + // What `authUIConfiguration { }` built inside a composable does: a fresh + // DefaultAuthUIStringProvider, identity-equal to nothing, on every recomposition. + @Suppress("UNUSED_EXPRESSION") + tick + val unstableProvider = DefaultAuthUIStringProvider(context) + CompositionLocalProvider(LocalAuthUIStringProvider provides unstableProvider) { + controller = rememberTopLevelDialogController { state } + controller.CurrentDialog() + } + } + + val error = AuthState.Error(Exception("boom")) + composeTestRule.runOnIdle { + state = error + controller.showErrorDialog( + exception = AuthException.from(error.exception, stringProvider) + ) + } + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText(stringProvider.errorDialogTitle).assertExists() + + composeTestRule.runOnIdle { tick++ } + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText(stringProvider.errorDialogTitle).assertExists() + } + + @Suppress("DEPRECATION") + @Test + fun `deprecated constructor still renders with its explicit provider and no CompositionLocal`() { + stringProvider = DefaultAuthUIStringProvider(ApplicationProvider.getApplicationContext()) + var state: AuthState = AuthState.Idle + lateinit var controller: TopLevelDialogController + + // Deliberately no LocalAuthUIStringProvider in scope: the local throws when absent, so + // this pins that the deprecated path keeps honouring the provider it was handed. + composeTestRule.setContent { + controller = rememberTopLevelDialogController(stringProvider) { state } + controller.CurrentDialog() + } + + val error = AuthState.Error(Exception("boom")) + composeTestRule.runOnIdle { + state = error + controller.showErrorDialog( + exception = AuthException.from(error.exception, stringProvider) + ) + } + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText(stringProvider.errorDialogTitle).assertExists() + } } diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/screens/email/EmailAuthRouteNavigationTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/screens/email/EmailAuthRouteNavigationTest.kt index 7d3abe4c62..c0552f9990 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/screens/email/EmailAuthRouteNavigationTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/screens/email/EmailAuthRouteNavigationTest.kt @@ -657,7 +657,7 @@ class EmailAuthRouteNavigationTest { } val authState by remember(authUI) { authUI.authStateFlow() }.collectAsState(AuthState.Idle) - val dialogController = rememberTopLevelDialogController(stringProvider) { authState } + val dialogController = rememberTopLevelDialogController { authState } CompositionLocalProvider( LocalAuthUIStringProvider provides configuration.stringProvider, diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreenVerificationLifecycleTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreenVerificationLifecycleTest.kt index 9624b2d47e..6a67f016b5 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreenVerificationLifecycleTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreenVerificationLifecycleTest.kt @@ -172,7 +172,7 @@ class PhoneAuthScreenVerificationLifecycleTest { */ private fun setScreenContent(withDialogs: Boolean = false) { composeTestRule.setContent { - val controller = rememberTopLevelDialogController(configuration.stringProvider) { + val controller = rememberTopLevelDialogController { AuthState.Idle } CompositionLocalProvider( @@ -202,8 +202,10 @@ class PhoneAuthScreenVerificationLifecycleTest { flowState = flowState, ) { state -> capturedState = state } } + // Inside the provider, like FirebaseAuthScreen: CurrentDialog resolves its + // strings from LocalAuthUIStringProvider at render time. + if (withDialogs) controller.CurrentDialog() } - if (withDialogs) controller.CurrentDialog() } composeTestRule.waitForIdle() } @@ -571,8 +573,9 @@ class PhoneAuthScreenVerificationLifecycleTest { settle() // The failure also tears down the verification, which must not append a second, - // spurious cancellation error behind the real one. - assertThat(reportedErrors.map { it.message }).containsExactly("sign-in blew up") + // spurious cancellation error behind the real one. AuthException.from replaces the + // message with renderable copy, so the original text is identified on the cause. + assertThat(reportedErrors.map { it.cause?.message }).containsExactly("sign-in blew up") } } diff --git a/auth/src/test/java/com/firebase/ui/auth/util/ContinueUrlBuilderTest.kt b/auth/src/test/java/com/firebase/ui/auth/util/ContinueUrlBuilderTest.kt new file mode 100644 index 0000000000..6b1b578138 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/util/ContinueUrlBuilderTest.kt @@ -0,0 +1,270 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.util + +import androidx.core.net.toUri +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Unit tests for [ContinueUrlBuilder]. Runs under Robolectric so the assertions can read the + * built URLs back through the real [android.net.Uri] parser rather than by string matching. + * + * @suppress Internal test class + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class ContinueUrlBuilderTest { + + @Test + fun `url without a query gets a question mark separator`() { + val url = ContinueUrlBuilder("https://example.com/finish") + .appendSessionId("sid123") + .appendAnonymousUserId("auid456") + .appendProviderId("google.com") + .appendForceSameDeviceBit(true) + .build() + + assertThat(url).isEqualTo( + "https://example.com/finish?ui_sid=sid123&ui_auid=auid456&ui_pid=google.com&ui_sd=1" + ) + + val uri = url.toUri() + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + assertThat(uri.getQueryParameter("ui_auid")).isEqualTo("auid456") + assertThat(uri.getQueryParameter("ui_pid")).isEqualTo("google.com") + assertThat(uri.getQueryParameter("ui_sd")).isEqualTo("1") + } + + @Test + fun `url with an existing query gets an ampersand separator and keeps the consumer's params`() { + val url = ContinueUrlBuilder("https://example.com/finish?demo=fullcustomization") + .appendSessionId("sid123") + .appendAnonymousUserId("auid456") + .appendProviderId("google.com") + .appendForceSameDeviceBit(true) + .build() + + assertThat(url.count { it == '?' }).isEqualTo(1) + + val uri = url.toUri() + assertThat(uri.getQueryParameter("demo")).isEqualTo("fullcustomization") + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + assertThat(uri.getQueryParameter("ui_auid")).isEqualTo("auid456") + assertThat(uri.getQueryParameter("ui_pid")).isEqualTo("google.com") + assertThat(uri.getQueryParameter("ui_sd")).isEqualTo("1") + } + + @Test + fun `url with several existing params keeps every one of them`() { + val url = ContinueUrlBuilder("https://example.com/finish?demo=full&lang=en") + .appendSessionId("sid123") + .build() + + val uri = url.toUri() + assertThat(uri.getQueryParameter("demo")).isEqualTo("full") + assertThat(uri.getQueryParameter("lang")).isEqualTo("en") + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + } + + @Test + fun `url ending in an open query marker keeps every param retrievable`() { + assertThat( + ContinueUrlBuilder("https://example.com/finish?").appendSessionId("sid123").build() + ).isEqualTo("https://example.com/finish?ui_sid=sid123") + + // A trailing `&` is an empty param, which Uri keeps rather than folding away. Cosmetic + // only: both the consumer's params and ours still parse out. + val url = ContinueUrlBuilder("https://example.com/finish?demo=full&") + .appendSessionId("sid123") + .build() + + assertThat(url).isEqualTo("https://example.com/finish?demo=full&&ui_sid=sid123") + + val uri = url.toUri() + assertThat(uri.getQueryParameter("demo")).isEqualTo("full") + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + } + + @Test + fun `param values are percent encoded`() { + val hostile = "a&b=c d#e" + val url = ContinueUrlBuilder("https://example.com/finish?demo=full") + .appendAnonymousUserId(hostile) + .build() + + // Interpolated raw, this value would split the query and open a fragment. + assertThat(url).doesNotContain(hostile) + + // Pin the encoded form on the wire, not just that it survives a round trip. + assertThat(url) + .isEqualTo("https://example.com/finish?demo=full&ui_auid=a%26b%3Dc%20d%23e") + + val uri = url.toUri() + assertThat(uri.getQueryParameter("ui_auid")).isEqualTo(hostile) + assertThat(uri.getQueryParameter("demo")).isEqualTo("full") + assertThat(uri.fragment).isNull() + } + + @Test + fun `params are appended before the fragment`() { + val url = ContinueUrlBuilder("https://example.com/finish?demo=full#section") + .appendSessionId("sid123") + .appendForceSameDeviceBit(false) + .build() + + assertThat(url) + .isEqualTo("https://example.com/finish?demo=full&ui_sid=sid123&ui_sd=0#section") + + val uri = url.toUri() + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + assertThat(uri.getQueryParameter("ui_sd")).isEqualTo("0") + assertThat(uri.fragment).isEqualTo("section") + } + + @Test + fun `fragment on a url without a query still gets a question mark separator`() { + val url = ContinueUrlBuilder("https://example.com/finish#section") + .appendSessionId("sid123") + .build() + + assertThat(url).isEqualTo("https://example.com/finish?ui_sid=sid123#section") + assertThat(url.toUri().getQueryParameter("ui_sid")).isEqualTo("sid123") + } + + @Test + fun `blank values are skipped`() { + val url = ContinueUrlBuilder("https://example.com/finish") + .appendSessionId("") + .appendAnonymousUserId(" ") + .appendProviderId("google.com") + .build() + + assertThat(url).isEqualTo("https://example.com/finish?ui_pid=google.com") + + val uri = url.toUri() + assertThat(uri.getQueryParameter("ui_sid")).isNull() + assertThat(uri.getQueryParameter("ui_auid")).isNull() + } + + @Test + fun `blank first value does not steal the separator from the next param`() { + val url = ContinueUrlBuilder("https://example.com/finish?demo=full") + .appendSessionId("") + .appendAnonymousUserId("auid456") + .build() + + assertThat(url).isEqualTo("https://example.com/finish?demo=full&ui_auid=auid456") + } + + @Test + fun `no params leaves the url untouched`() { + assertThat(ContinueUrlBuilder("https://example.com/finish").build()) + .isEqualTo("https://example.com/finish") + + assertThat(ContinueUrlBuilder("https://example.com/finish?demo=full").build()) + .isEqualTo("https://example.com/finish?demo=full") + + assertThat(ContinueUrlBuilder("https://example.com/finish?demo=full#section").build()) + .isEqualTo("https://example.com/finish?demo=full#section") + } + + @Test + fun `multi segment path is untouched`() { + val url = ContinueUrlBuilder("https://example.com/finish/fullcustomization") + .appendSessionId("sid123") + .build() + + assertThat(url).isEqualTo("https://example.com/finish/fullcustomization?ui_sid=sid123") + + val uri = url.toUri() + assertThat(uri.lastPathSegment).isEqualTo("fullcustomization") + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + } + + @Test + fun `force same device bit is written as one or zero`() { + assertThat( + ContinueUrlBuilder("https://example.com/finish").appendForceSameDeviceBit(true).build() + ).isEqualTo("https://example.com/finish?ui_sd=1") + + assertThat( + ContinueUrlBuilder("https://example.com/finish").appendForceSameDeviceBit(false).build() + ).isEqualTo("https://example.com/finish?ui_sd=0") + } + + @Test + fun `query ending in an unencoded question mark still keeps every param`() { + // An unencoded '?' is legal inside a query (RFC 3986), so a trailing one does + // not mean the query is still open. + val input = "https://example.com/finish?next=/search?" + val url = ContinueUrlBuilder(input) + .appendSessionId("sid123") + .appendForceSameDeviceBit(true) + .build() + + val uri = url.toUri() + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + assertThat(uri.getQueryParameter("ui_sd")).isEqualTo("1") + assertThat(uri.getQueryParameter("next")).isEqualTo("/search?") + } + + @Test + fun `url ending in a bare double question mark keeps every param`() { + val url = ContinueUrlBuilder("https://example.com/finish??") + .appendSessionId("sid123") + .build() + + assertThat(url.toUri().getQueryParameter("ui_sid")).isEqualTo("sid123") + } + + @Test + fun `fragment before a query keeps the params in the query`() { + // Everything after the first '#' is the fragment, query-looking or not. + val url = ContinueUrlBuilder("https://example.com/finish#section?x=1") + .appendSessionId("sid123") + .build() + + val uri = url.toUri() + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + assertThat(uri.fragment).isEqualTo("section?x=1") + } + + @Test + fun `percent encoded url in a query value survives untouched`() { + val input = "https://example.com/finish?redirect=https%3A%2F%2Ffoo.com%2Fa%3Fb%3Dc" + val url = ContinueUrlBuilder(input).appendSessionId("sid123").build() + + val uri = url.toUri() + assertThat(uri.getQueryParameter("redirect")).isEqualTo("https://foo.com/a?b=c") + assertThat(uri.getQueryParameter("ui_sid")).isEqualTo("sid123") + } + + @Test + fun `the consumer's url is preserved verbatim ahead of the appended params`() { + val input = "https://example.com/finish?demo=full&lang=en" + val url = ContinueUrlBuilder(input).appendSessionId("sid123").build() + + assertThat(url).startsWith(input) + } + + @Test(expected = IllegalArgumentException::class) + fun `blank url is rejected`() { + ContinueUrlBuilder(" ") + } +} diff --git a/auth/src/test/java/com/firebase/ui/auth/util/EmailLinkParserTest.kt b/auth/src/test/java/com/firebase/ui/auth/util/EmailLinkParserTest.kt new file mode 100644 index 0000000000..ee831f5d05 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/util/EmailLinkParserTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.util + +import androidx.core.net.toUri +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Unit tests for [EmailLinkParser], including round-trips of the continue URLs that + * [ContinueUrlBuilder] produces. + * + * @suppress Internal test class + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class EmailLinkParserTest { + + @Test + fun `parses the parameters of a continue url built from a url without a query`() { + val parser = EmailLinkParser( + ContinueUrlBuilder("https://example.com/finish") + .appendSessionId("sid123") + .appendAnonymousUserId("auid456") + .appendProviderId("google.com") + .appendForceSameDeviceBit(true) + .build() + ) + + assertThat(parser.sessionId).isEqualTo("sid123") + assertThat(parser.anonymousUserId).isEqualTo("auid456") + assertThat(parser.providerId).isEqualTo("google.com") + assertThat(parser.forceSameDeviceBit).isTrue() + } + + @Test + fun `parses the parameters of a continue url built from a url that already had a query`() { + val parser = EmailLinkParser( + ContinueUrlBuilder("https://example.com/finish?demo=fullcustomization") + .appendSessionId("sid123") + .appendAnonymousUserId("auid456") + .appendProviderId("google.com") + .appendForceSameDeviceBit(true) + .build() + ) + + assertThat(parser.sessionId).isEqualTo("sid123") + assertThat(parser.anonymousUserId).isEqualTo("auid456") + assertThat(parser.providerId).isEqualTo("google.com") + assertThat(parser.forceSameDeviceBit).isTrue() + } + + @Test + fun `parses the parameters of a continue url that had a query and a fragment`() { + val parser = EmailLinkParser( + ContinueUrlBuilder("https://example.com/finish?demo=fullcustomization#section") + .appendSessionId("sid123") + .appendForceSameDeviceBit(false) + .build() + ) + + assertThat(parser.sessionId).isEqualTo("sid123") + assertThat(parser.forceSameDeviceBit).isFalse() + } + + @Test + fun `parses the parameters out of the continue url nested in an email link`() { + val continueUrl = ContinueUrlBuilder("https://example.com/finish?demo=fullcustomization") + .appendSessionId("sid123") + .appendAnonymousUserId("auid456") + .appendForceSameDeviceBit(true) + .build() + + val emailLink = "https://example.firebaseapp.com/__/auth/action".toUri() + .buildUpon() + .appendQueryParameter("mode", "signIn") + .appendQueryParameter("oobCode", "oob789") + .appendQueryParameter("continueUrl", continueUrl) + .build() + .toString() + + val parser = EmailLinkParser(emailLink) + + assertThat(parser.oobCode).isEqualTo("oob789") + assertThat(parser.sessionId).isEqualTo("sid123") + assertThat(parser.anonymousUserId).isEqualTo("auid456") + assertThat(parser.forceSameDeviceBit).isTrue() + } + + @Test + fun `missing optional parameters read back as null`() { + val parser = EmailLinkParser( + ContinueUrlBuilder("https://example.com/finish").appendSessionId("sid123").build() + ) + + assertThat(parser.sessionId).isEqualTo("sid123") + assertThat(parser.anonymousUserId).isNull() + assertThat(parser.providerId).isNull() + // The bit defaults to false when the link does not carry one. + assertThat(parser.forceSameDeviceBit).isFalse() + } + + @Test + fun `parses the parameters out of a continue url nested behind a dynamic link`() { + val continueUrl = ContinueUrlBuilder("https://example.com/finish?demo=fullcustomization") + .appendSessionId("sid123") + .appendAnonymousUserId("auid456") + .build() + + val actionLink = "https://example.firebaseapp.com/__/auth/action".toUri() + .buildUpon() + .appendQueryParameter("mode", "signIn") + .appendQueryParameter("oobCode", "oob789") + .appendQueryParameter("continueUrl", continueUrl) + .build() + .toString() + + // Exercises parseUri's `link=` branch, not just `continueUrl=`. + val dynamicLink = "https://example.page.link/x".toUri() + .buildUpon() + .appendQueryParameter("link", actionLink) + .build() + .toString() + + val parser = EmailLinkParser(dynamicLink) + + assertThat(parser.oobCode).isEqualTo("oob789") + assertThat(parser.sessionId).isEqualTo("sid123") + assertThat(parser.anonymousUserId).isEqualTo("auid456") + } + + @Test(expected = IllegalArgumentException::class) + fun `blank link is rejected`() { + EmailLinkParser(" ") + } + + @Test(expected = IllegalArgumentException::class) + fun `link without parameters is rejected`() { + EmailLinkParser("https://example.com/finish") + } + + @Test(expected = IllegalArgumentException::class) + fun `missing oob code is rejected when read`() { + EmailLinkParser("https://example.com/finish?ui_sid=sid123").oobCode + } +} diff --git a/build.gradle.kts b/build.gradle.kts index e5e280ced7..c5ff470598 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,9 @@ @file:Suppress("UnstableApiUsage") +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.dsl.Lint + plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false @@ -31,18 +35,44 @@ allprojects { } } -// Android Lint is configured per module, so there is no repo-wide entry point by default. -// This task is that entry point, and the module list is the gate's definition: -// - :app and :e2eTest declare no lint { } block yet, so they are deliberately absent (CPRN-433). +// The shared Android Lint policy, alongside the checkstyle one above. Modules add only their +// own disables; the strictness flags live here so a module cannot quietly opt out of the gate +// the way :library did with abortOnError = false. +fun Lint.applyCommonPolicy() { + disable += setOf( + "IconExpectedSize", + "InvalidPackage", // Firestore uses GRPC which makes lint mad + "NewerVersionAvailable", "GradleDependency", // For reproducible builds + "SelectableText", "SyntheticAccessor" // We almost never care about this + ) + + checkAllWarnings = true + warningsAsErrors = true + abortOnError = true +} + +subprojects { + plugins.withId("com.android.application") { + extensions.configure { lint { applyCommonPolicy() } } + } + plugins.withId("com.android.library") { + extensions.configure { lint { applyCommonPolicy() } } + } +} + +// Android Lint has no repo-wide entry point by default. This task is that entry point, and +// the module list is the gate's definition: // - :proguard-tests disables its debug variant on CI, so it is gated on release instead. tasks.register("lintAll") { group = "verification" - description = "Runs Android Lint for every module that configures a lint { } block." + description = "Runs Android Lint for every module gated on it." dependsOn( + ":app:lintDebug", ":auth:lintDebug", ":common:lintDebug", ":database:lintDebug", + ":e2eTest:lintDebug", ":firestore:lintDebug", ":library:lintDebug", ":storage:lintDebug", diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 7b8e3c497b..5fcb8e613f 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -23,20 +23,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } buildTypes { named("release").configure { diff --git a/database/build.gradle.kts b/database/build.gradle.kts index 7e308b9e57..763627927f 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -25,20 +25,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } buildTypes { named("release").configure { diff --git a/e2eTest/build.gradle.kts b/e2eTest/build.gradle.kts index e54f6003d1..b7b294807c 100644 --- a/e2eTest/build.gradle.kts +++ b/e2eTest/build.gradle.kts @@ -14,6 +14,7 @@ android { minSdk = Config.SdkVersions.min } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt index 838bac0f71..f74ce4ad87 100644 --- a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt +++ b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt @@ -6,6 +6,7 @@ import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -65,6 +66,8 @@ import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import androidx.credentials.PasswordCredential as AndroidPasswordCredential +private const val SIGN_OUT_BUTTON_LABEL = "SIGN OUT" + @Config(sdk = [34]) @RunWith(RobolectricTestRunner::class) class EmailAuthScreenTest { @@ -336,6 +339,100 @@ class EmailAuthScreenTest { assertThat(authUI.auth.currentUser!!.email).isEqualTo(email) } + @Test + fun `sign out from the authenticated screen clears the Firebase session`() { + val email = "signout-test-${System.currentTimeMillis()}@example.com" + val password = "test123" + + val user = ensureFreshUser(authUI, email, password) + requireNotNull(user) { "Failed to create user" } + + try { + verifyEmailInEmulator(authUI, emulatorApi, user) + } catch (e: Exception) { + Assume.assumeTrue( + "Skipping test: Firebase Auth Emulator OOB codes endpoint not available. " + + "Ensure emulator is running on localhost:9099. Error: ${e.message}", + false + ) + } + + authUI.auth.signOut() + shadowOf(Looper.getMainLooper()).idle() + + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + isCredentialManagerEnabled = false + } + + var currentAuthState: AuthState = AuthState.Idle + + composeAndroidTestRule.setContent { + CompositionLocalProvider( + LocalAuthUIStringProvider provides DefaultAuthUIStringProvider(applicationContext) + ) { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = { }, + onSignInFailure = { }, + onSignInCancelled = { }, + // Drive FirebaseAuthUI.signOut() through the production callback rather than + // FirebaseAuth.signOut(): this module has no Facebook SDK on its classpath, so + // it also pins that signing out an email-only user touches no Facebook types. + authenticatedContent = { _, uiContext -> + Button(onClick = uiContext.onSignOut) { + Text(SIGN_OUT_BUTTON_LABEL) + } + } + ) + } + val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) + currentAuthState = authState + } + + assertDirectEmailStart() + + composeAndroidTestRule.onNodeWithText(stringProvider.emailHint) + .performScrollTo() + .performTextInput(email) + composeAndroidTestRule.onNodeWithText(stringProvider.passwordHint) + .performScrollTo() + .performTextInput(password) + composeAndroidTestRule.onNodeWithText(stringProvider.signInDefault.uppercase()) + .performScrollTo() + .performClick() + + shadowOf(Looper.getMainLooper()).idle() + composeAndroidTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + currentAuthState is AuthState.Success + } + shadowOf(Looper.getMainLooper()).idle() + + composeAndroidTestRule.onNodeWithText(SIGN_OUT_BUTTON_LABEL) + .assertIsDisplayed() + .performClick() + + shadowOf(Looper.getMainLooper()).idle() + composeAndroidTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + currentAuthState is AuthState.Idle + } + shadowOf(Looper.getMainLooper()).idle() + + assertThat(currentAuthState).isInstanceOf(AuthState.Idle::class.java) + assertThat(authUI.auth.currentUser).isNull() + } + @Test fun `new email sign-up emits RequiresEmailVerification auth state`() { val name = "Test User" diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index 83ad1617df..e2a6feb39d 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -24,20 +24,6 @@ android { } } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } buildTypes { named("release").configure { diff --git a/internal/lintchecks/build.gradle.kts b/internal/lintchecks/build.gradle.kts index fc24e7762f..aec59f0529 100644 --- a/internal/lintchecks/build.gradle.kts +++ b/internal/lintchecks/build.gradle.kts @@ -23,21 +23,6 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } } dependencies { diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 8cad8e6b05..d709bba4a6 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -23,20 +23,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } } dependencies { diff --git a/okf-bundle/ci-workflows/android.md b/okf-bundle/ci-workflows/android.md index 92631d23ea..7463b44e76 100644 --- a/okf-bundle/ci-workflows/android.md +++ b/okf-bundle/ci-workflows/android.md @@ -35,10 +35,23 @@ Canonical owner for the CI unit-path step list. Script: [scripts/build.sh](../.. 1. Copy `library/google-services.json` → `app/` and `proguard-tests/` 2. `./gradlew --max-workers=2 clean` 3. `./gradlew --max-workers=2 assembleDebug` -4. `./gradlew --max-workers=2 checkstyle` -5. `./gradlew --max-workers=2 testDebugUnitTest -x :e2eTest:testDebugUnitTest` +4. `./gradlew --max-workers=2 proguard-tests:build` — the R8 gate, [detail below](#proguard-step) +5. `./gradlew --max-workers=2 checkstyle` +6. `./gradlew --max-workers=2 testDebugUnitTest -x :e2eTest:testDebugUnitTest` -Step 4 is **Java-only** (`include("**/*.java")`), so it inspects zero files in the Kotlin modules — [Kotlin blind spot](../testing/agent-command-policy.md#checkstyle-kotlin-blind-spot). Android Lint covers that gap and runs in its own workflow, **not** in `build.sh` — see below. +Step 5 is **Java-only** (`include("**/*.java")`), so it inspects zero files in the Kotlin modules — [Kotlin blind spot](../testing/agent-command-policy.md#checkstyle-kotlin-blind-spot). Android Lint covers that gap and runs in its own workflow, **not** in `build.sh` — see below. + + + +### Step 4: the R8 gate + +`:proguard-tests` is a source-less application module that depends on `:auth`, `:firestore`, `:database` and `:storage` and builds its release variant with `isMinifyEnabled = true`, so `minifyReleaseWithR8` is the only place the libraries' consumer ProGuard rules are ever applied. Each library's own release build sets `isMinifyEnabled = false`. `consumerProguardFiles` is declared by `:auth` (`auth-proguard.pro`), `:firestore` and `:database` (`proguard-rules.pro` each); `:storage` and `:common` declare none, though AGP also merges rules generated by annotation processors. The gate is sensitive: removing `-dontwarn com.facebook.**` from `auth-proguard.pro` fails `minifyReleaseWithR8`, because `:auth` takes `facebook-login` as `compileOnly`. It proves the rules shrink and package, not that the shrunk app behaves correctly at runtime. + +The module's `beforeVariants` filter disables its **debug** variant when `CI=true`, so CI builds the release variant only; a local run of the script builds both. + + + +**Lint footprint.** `build` depends on `check`, which depends on `lint`, so this step runs Android Lint too. On CI that is `:proguard-tests:lintRelease`; locally, with `CI` unset, it is `:proguard-tests:lintDebug` — the ungated debug variant [`lintAll` does not cover](../testing/agent-command-policy.md#canonical-registry), so a debug-only finding can redden a local `build.sh` while CI stays green. Either way lint **analysis** fans out to the library dependencies (`:auth`, `:common`, `:database`, `:firestore`, `:storage`, plus `:internal:lintchecks` locally, since `:auth` takes it as `debugImplementation`), because AGP builds a lint model for each; only the **report** is scoped to `:proguard-tests`. Two consequences: it duplicates analysis [`lintAll`](#lint-workflow) already does, and since the step sits at position 4 of 6 under `set -e`, a finding here aborts the run before `checkstyle` and before every unit-test result — the same masking that kept lint out of this script in the first place (below). Drop to `:proguard-tests:assembleRelease` if that trade stops being worth it. @@ -46,14 +59,12 @@ Step 4 is **Java-only** (`include("**/*.java")`), so it inspects zero files in t Separate workflow, `pull_request` only, running `./gradlew --max-workers=2 lintAll`. -`lintAll` is registered in the root `build.gradle.kts` and gates the 8 modules that configure a `lint { }` block; `:app` and `:e2eTest` are not yet among them (CPRN-433). +`lintAll` is registered in the root `build.gradle.kts` and gates all 10 Android modules. The strictness flags and the common `disable` set live in a shared policy in that same file, applied to every module that applies the application or library plugin; a module's own `lint { }` block carries only its module-specific disables. It is a separate workflow rather than a step in `build.sh` for two reasons. Lint measured **~4-5 minutes** on this repo — the `build` job went from 3-6 min to 8-11 min when it was inline — so running it in parallel roughly halves PR feedback time at about the same total runner cost, since the extra compile the lint job pays is the one `build.sh` stops paying (`lintAnalyze` depends on `compileDebugKotlin`, and there is no remote build cache: Develocity here is configured for build scans only). And under `set -e` an inline lint failure aborted the run **before** `testDebugUnitTest`, so one new finding cost you every test result for that run. The workflow copies `library/google-services.json` into `app/` and `proguard-tests/` before running, because `lintAll` gates `:proguard-tests:lintRelease` and that module applies the `google-services` plugin. -`proguard-tests:build` is currently commented out (re-enable before release). Green Android CI does **not** prove ProGuard/R8 packaging. - ## Agent notes - Match this path locally with `./scripts/build.sh` — [agent command policy](../testing/agent-command-policy.md). diff --git a/okf-bundle/modules/index.md b/okf-bundle/modules/index.md index 7c5f368643..92a6208457 100644 --- a/okf-bundle/modules/index.md +++ b/okf-bundle/modules/index.md @@ -21,7 +21,7 @@ Version and SDK floors: `buildSrc/.../Config.kt` — [repo tooling](../repo-tool | `:library` | Umbrella / publish aggregation (`prepareArtifacts`) | | `:app` | Demo app (Auth Compose sample) | | `:e2eTest` | Auth emulator e2e (Robolectric + Compose UI test) | -| `:proguard-tests` | R8/ProGuard packaging checks (disabled in CI unit path — [Android CI](../ci-workflows/android.md)) | +| `:proguard-tests` | R8/ProGuard packaging gate for the libraries' consumer rules; run by `build.sh` — [Android CI](../ci-workflows/android.md#proguard-step) | | `:lint`, `:internal:lint`, `:internal:lintchecks` | Custom lint detectors — [what each is for](#custom-lint-modules) | | `buildSrc` | Shared `Config` (version, SDK levels, submodule list) | diff --git a/okf-bundle/testing/agent-command-policy.md b/okf-bundle/testing/agent-command-policy.md index 5e052d9246..eded881730 100644 --- a/okf-bundle/testing/agent-command-policy.md +++ b/okf-bundle/testing/agent-command-policy.md @@ -26,12 +26,12 @@ Single source for **which shell commands agents may run** in this repo. E2e is a | Intent | Command | Never use instead | |--------|---------|-------------------| -| Full CI unit path (assemble + checkstyle + unit tests) | `./scripts/build.sh` | Ad-hoc `./gradlew clean assembleDebug test` without checkstyle; inventing a different exclusion set | +| Full CI unit path (assemble + R8 + checkstyle + unit tests) | `./scripts/build.sh` | Ad-hoc `./gradlew clean assembleDebug test` without checkstyle; inventing a different exclusion set | | Unit tests (all library modules; exclude e2eTest) | `./gradlew testDebugUnitTest -x :e2eTest:testDebugUnitTest` | Bare `./gradlew test` (pulls wrong tasks / e2e); IDE-only as the agent gate | | Unit tests (one module with a real `src/test` suite) | `./gradlew ::testDebugUnitTest` (e.g. `:auth:testDebugUnitTest`, `:firestore:…`, `:storage:…`) | `:common:testDebugUnitTest` / `:database:testDebugUnitTest` as “green” evidence (empty suites — [empty unit-suite trap](#empty-unit-suite-trap)); full suite when only one module changed *as a substitute for* the CI path at handoff | | Assemble one module (when no JVM unit suite) | `./gradlew ::assembleDebug` (e.g. `:database`, `:common`) | Treating empty `testDebugUnitTest` as validation | | Checkstyle (**Java only**) | `./gradlew checkstyle` | Invented ktlint/detekt entrypoints; treating a green checkstyle as style coverage for Kotlin sources ([Kotlin blind spot](#checkstyle-kotlin-blind-spot)) | -| Android Lint (all gated modules) — **not** in `build.sh`, own workflow | `./gradlew lintAll` | Bare `./gradlew lint` / `lintDebug` (pulls `:app` and `:e2eTest`, which declare no `lint { }` block yet); assuming a green `build.sh` covered lint | +| Android Lint (all gated modules) — **not** in `build.sh`, own workflow | `./gradlew lintAll` | Bare `./gradlew lint` / `lintDebug` (pulls variants `lintAll` does not gate, e.g. `:proguard-tests` debug); assuming a green `build.sh` covered lint | | Android Lint (one module) | `./gradlew ::lintDebug` (`:proguard-tests` uses `lintRelease`) | Editing Kotlin in a gated module without re-running lint | | Accept new lint debt (**needs a human decision**) | `./gradlew :auth:updateLintBaseline` | Running this to make a red build green — see [lint baseline trap](#lint-baseline-trap) | | Assemble debug | `./gradlew assembleDebug` | Module-scoped assemble as the only CI substitute at handoff | @@ -122,9 +122,10 @@ Single source for **which shell commands agents may run** in this repo. E2e is a - [.github/PULL_REQUEST_TEMPLATE.md](../../.github/PULL_REQUEST_TEMPLATE.md) mentions `./gradlew check` (stale vs current CI). - **Agents:** treat **`./scripts/build.sh`** as the CI-matching unit path — [Android CI](../ci-workflows/android.md). Full handoff (including e2e when Auth UI touched): [validation checklist](validation-checklist.md). -### ProGuard tests disabled in build.sh +### `proguard-tests:build` is the only R8 gate -- See [Android CI § `build.sh`](../ci-workflows/android.md#what-buildsh-runs) — `proguard-tests:build` is commented out; green unit CI does not prove ProGuard/R8. +- `build.sh` runs it, and it is the only task that applies the libraries' consumer ProGuard rules — [Android CI § the ProGuard step](../ci-workflows/android.md#proguard-step). +- Do **not** comment it out to clear a red build. A failure there is a real defect in the consumer rules or in code they must keep; fix the rules. ### Emulator foreground vs CI diff --git a/okf-bundle/testing/validation-checklist.md b/okf-bundle/testing/validation-checklist.md index f8764672e1..f04eca27b4 100644 --- a/okf-bundle/testing/validation-checklist.md +++ b/okf-bundle/testing/validation-checklist.md @@ -26,7 +26,7 @@ Work types and tiers: [change authoring workflow](change-authoring-workflow.md). ## Build and unit tests -Repo root. Full CI unit path (what `build.sh` runs — `assembleDebug`, `checkstyle`, unit tests): [Android CI](../ci-workflows/android.md). Lint and e2e are **separate** workflows; `build.sh` does not run them. +Repo root. Full CI unit path (what `build.sh` runs — `assembleDebug`, `proguard-tests:build`, `checkstyle`, unit tests): [Android CI](../ci-workflows/android.md). Lint and e2e are **separate** workflows; `build.sh` does not run `lintAll` or `e2eTest`. It is not lint-free, though: `proguard-tests:build` pulls that module's lint through `check`, and lint **analysis** with it for the library dependencies (`:auth`, `:common`, `:database`, `:firestore`, `:storage`, and `:internal:lintchecks` on a local run) — [lint footprint](../ci-workflows/android.md#proguard-step-lint). Findings are reported for `:proguard-tests` only, so a lint failure during `build.sh` is still that module's gate, not `lintAll`'s. ```bash ./scripts/build.sh @@ -62,9 +62,9 @@ Instrumented `androidTest` (database/firestore) is **not** in CI or the agent al ./gradlew lintAll # Android Lint — reads Kotlin and resources ``` -`checkstyle` is scoped `include("**/*.java")` from the root `build.gradle.kts`, so on a Kotlin-only diff it inspects **zero files and exits 0**. A green checkstyle is not evidence for a change in `:auth`, `:app` or `:e2eTest` — [Kotlin blind spot](agent-command-policy.md#checkstyle-kotlin-blind-spot). +`checkstyle` is scoped `include("**/*.java")` from the root `build.gradle.kts`, so on a Kotlin-only diff it inspects **zero files and exits 0**. A green checkstyle is not evidence for a change in `:auth`, `:app` or `:e2eTest`; `lintAll` is what covers those — [Kotlin blind spot](agent-command-policy.md#checkstyle-kotlin-blind-spot). -`lintAll` runs Android Lint for the 8 modules that configure a `lint { }` block, each at `checkAllWarnings = true`, `warningsAsErrors = true` and `abortOnError = true` — so any new finding fails the build. It runs in its own workflow ([lint.yml](../ci-workflows/android.md#lint-workflow)), **not** in `build.sh`, so you must run it separately — a green `build.sh` says nothing about lint. `:app` and `:e2eTest` are not yet gated (CPRN-433). Config: each module's `lint { }` block; `library/quality/checkstyle.xml` for checkstyle. +`lintAll` runs Android Lint for all 10 Android modules at `checkAllWarnings = true`, `warningsAsErrors = true` and `abortOnError = true` — so any new finding fails the build. It runs in its own workflow ([lint.yml](../ci-workflows/android.md#lint-workflow)), **not** in `build.sh`, so you must run it separately — a green `build.sh` covers only `:proguard-tests`' own findings (see above), never the other nine modules'. Config: the shared policy in the root `build.gradle.kts` sets those flags and the common `disable` set, and a module's own `lint { }` block adds only its module-specific disables; `library/quality/checkstyle.xml` for checkstyle. `auth/lint-baseline.xml` suppresses 180 pre-existing findings. **Never** run `updateLintBaseline` to clear a failure your change caused — [baseline trap](agent-command-policy.md#lint-baseline-trap). @@ -114,7 +114,7 @@ Before closing **`implementation_gate`**, **`review_gate`**, **`commit_gate`**, ## Handoff checklist -- [ ] `./scripts/build.sh` (or equivalent assemble + checkstyle + unit exclusion path) exit 0 +- [ ] `./scripts/build.sh` (or equivalent assemble + R8 + checkstyle + unit exclusion path) exit 0 - [ ] Module evidence per [module validation matrix](#module-validation-matrix) - [ ] `./gradlew checkstyle` when **Java** sources changed - [ ] `./gradlew lintAll` when Kotlin or resources changed in a gated module diff --git a/proguard-tests/build.gradle.kts b/proguard-tests/build.gradle.kts index 0a419b7402..d76d08a70c 100644 --- a/proguard-tests/build.gradle.kts +++ b/proguard-tests/build.gradle.kts @@ -46,20 +46,11 @@ android { } lint { - // Common lint options across all modules + // Module specific disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor", // We almost never care about this "MediaCapabilities", "MissingApplicationIcon" ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - } androidComponents { diff --git a/scripts/build.sh b/scripts/build.sh index b216f04b76..6e406a7247 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,8 +8,7 @@ cp library/google-services.json proguard-tests/google-services.json ./gradlew $GRADLE_ARGS clean ./gradlew $GRADLE_ARGS assembleDebug -# TODO(thatfiredev): re-enable before release -# ./gradlew $GRADLE_ARGS proguard-tests:build +./gradlew $GRADLE_ARGS proguard-tests:build ./gradlew $GRADLE_ARGS checkstyle # Android Lint is the Kotlin-capable gate, but it runs in its own workflow # (.github/workflows/lint.yml) so it runs in parallel with this path rather than diff --git a/storage/build.gradle.kts b/storage/build.gradle.kts index 0e6f8af175..af04a43c78 100644 --- a/storage/build.gradle.kts +++ b/storage/build.gradle.kts @@ -23,21 +23,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } - buildTypes { named("release").configure { isMinifyEnabled = false @@ -55,4 +40,4 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.mockito.core) -} \ No newline at end of file +}