Skip to content

Commit 3dcd719

Browse files
committed
feat: Anonymous sign in and auto upgrade
1 parent 5a13fbc commit 3dcd719

File tree

7 files changed

+219
-80
lines changed

7 files changed

+219
-80
lines changed

auth/src/main/AndroidManifest.xml

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -122,27 +122,6 @@
122122
<data android:scheme="@string/facebook_login_protocol_scheme" />
123123
</intent-filter>
124124
</activity>
125-
126-
<!-- Email Link Sign-In Handler Activity for Compose -->
127-
<!-- This activity handles deep links for passwordless email authentication -->
128-
<!-- The host is automatically read from firebase_web_host in config.xml -->
129-
<!-- NOTE: firebase_web_host must be lowercase (e.g., project-id.firebaseapp.com) -->
130-
<activity
131-
android:name=".compose.ui.screens.EmailSignInLinkHandlerActivity"
132-
android:label=""
133-
android:exported="true"
134-
android:theme="@style/FirebaseUI.Transparent">
135-
<intent-filter android:autoVerify="true">
136-
<action android:name="android.intent.action.VIEW" />
137-
<category android:name="android.intent.category.DEFAULT" />
138-
<category android:name="android.intent.category.BROWSABLE" />
139-
<data
140-
android:scheme="https"
141-
android:host="@string/firebase_web_host"
142-
tools:ignore="AppLinksAutoVerify,AppLinkUrlError" />
143-
</intent-filter>
144-
</activity>
145-
146125
<provider
147126
android:name=".data.client.AuthUiInitProvider"
148127
android:authorities="${applicationId}.authuiinitprovider"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.firebase.ui.auth.compose.configuration.auth_provider
2+
3+
import com.firebase.ui.auth.compose.AuthException
4+
import com.firebase.ui.auth.compose.AuthState
5+
import com.firebase.ui.auth.compose.FirebaseAuthUI
6+
import kotlinx.coroutines.CancellationException
7+
import kotlinx.coroutines.tasks.await
8+
9+
10+
/**
11+
* Signs in a user anonymously with Firebase Authentication.
12+
*
13+
* This method creates a temporary anonymous user account that can be used for testing
14+
* or as a starting point for users who want to try the app before creating a permanent
15+
* account. Anonymous users can later be upgraded to permanent accounts by linking
16+
* credentials (email/password, social providers, phone, etc.).
17+
*
18+
* **Flow:**
19+
* 1. Updates auth state to loading with "Signing in anonymously..." message
20+
* 2. Calls Firebase Auth's `signInAnonymously()` method
21+
* 3. Updates auth state to idle on success
22+
* 4. Handles cancellation and converts exceptions to [AuthException] types
23+
*
24+
* **Anonymous Account Benefits:**
25+
* - No user data collection required
26+
* - Immediate access to app features
27+
* - Can be upgraded to permanent account later
28+
* - Useful for guest users and app trials
29+
*
30+
* **Account Upgrade:**
31+
* Anonymous accounts can be upgraded to permanent accounts by calling methods like:
32+
* - [signInAndLinkWithCredential] with email/password or social credentials
33+
* - [createOrLinkUserWithEmailAndPassword] for email/password accounts
34+
* - [signInWithPhoneAuthCredential] for phone authentication
35+
*
36+
* **Example: Basic anonymous sign-in**
37+
* ```kotlin
38+
* try {
39+
* firebaseAuthUI.signInAnonymously()
40+
* // User is now signed in anonymously
41+
* // Show app content or prompt for account creation
42+
* } catch (e: AuthException.AuthCancelledException) {
43+
* // User cancelled the sign-in process
44+
* } catch (e: AuthException.NetworkException) {
45+
* // Network error occurred
46+
* }
47+
* ```
48+
*
49+
* **Example: Anonymous sign-in with upgrade flow**
50+
* ```kotlin
51+
* // Step 1: Sign in anonymously
52+
* firebaseAuthUI.signInAnonymously()
53+
*
54+
* // Step 2: Later, upgrade to permanent account
55+
* try {
56+
* firebaseAuthUI.createOrLinkUserWithEmailAndPassword(
57+
* context = context,
58+
* config = authUIConfig,
59+
* provider = emailProvider,
60+
* name = "John Doe",
61+
* email = "[email protected]",
62+
* password = "SecurePass123!"
63+
* )
64+
* // Anonymous account upgraded to permanent email/password account
65+
* } catch (e: AuthException.AccountLinkingRequiredException) {
66+
* // Email already exists - show account linking UI
67+
* }
68+
* ```
69+
*
70+
* @throws AuthException.AuthCancelledException if the coroutine is cancelled
71+
* @throws AuthException.NetworkException if a network error occurs
72+
* @throws AuthException.UnknownException for other authentication errors
73+
*
74+
* @see signInAndLinkWithCredential for upgrading anonymous accounts
75+
* @see createOrLinkUserWithEmailAndPassword for email/password upgrade
76+
* @see signInWithPhoneAuthCredential for phone authentication upgrade
77+
*/
78+
suspend fun FirebaseAuthUI.signInAnonymously() {
79+
try {
80+
updateAuthState(AuthState.Loading("Signing in anonymously..."))
81+
auth.signInAnonymously().await()
82+
updateAuthState(AuthState.Idle)
83+
} catch (e: CancellationException) {
84+
val cancelledException = AuthException.AuthCancelledException(
85+
message = "Sign in anonymously was cancelled",
86+
cause = e
87+
)
88+
updateAuthState(AuthState.Error(cancelledException))
89+
throw cancelledException
90+
} catch (e: AuthException) {
91+
updateAuthState(AuthState.Error(e))
92+
throw e
93+
} catch (e: Exception) {
94+
val authException = AuthException.from(e)
95+
updateAuthState(AuthState.Error(authException))
96+
throw authException
97+
}
98+
}

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import kotlinx.coroutines.launch
5050
*
5151
* @see signInWithFacebook
5252
*/
53+
// TODO(demolaf): make this internal after testing with compose app
5354
@Composable
5455
fun FirebaseAuthUI.rememberSignInWithFacebookLauncher(
5556
context: Context,

auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ fun AuthMethodPicker(
122122
}
123123
}
124124
AnnotatedStringResource(
125+
modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp),
125126
context = context,
126127
inPreview = inPreview,
127128
previewText = "By continuing, you accept our Terms of Service and Privacy Policy.",
128-
modifier = Modifier.padding(vertical = 16.dp),
129129
id = R.string.fui_tos_and_pp,
130130
links = arrayOf(
131131
"Terms of Service" to (termsOfServiceUrl ?: ""),

composeapp/src/main/AndroidManifest.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@
2222

2323
<category android:name="android.intent.category.LAUNCHER" />
2424
</intent-filter>
25+
26+
<!-- Email Link Sign-In Handler Activity for Compose -->
27+
<!-- This activity handles deep links for passwordless email authentication -->
28+
<!-- The host is automatically read from firebase_web_host in config.xml -->
29+
<!-- NOTE: firebase_web_host must be lowercase (e.g., project-id.firebaseapp.com) -->
30+
<intent-filter android:autoVerify="true">
31+
<action android:name="android.intent.action.VIEW" />
32+
<category android:name="android.intent.category.DEFAULT" />
33+
<category android:name="android.intent.category.BROWSABLE" />
34+
<data
35+
android:scheme="https"
36+
android:host="@string/firebase_web_host"
37+
android:pathPattern="/__/auth/links"
38+
tools:ignore="AppLinksAutoVerify,AppLinkUrlError" />
39+
</intent-filter>
2540
</activity>
2641
</application>
2742

composeapp/src/main/java/com/firebase/composeapp/MainActivity.kt

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.firebase.composeapp
22

3+
import android.content.Context
34
import android.os.Bundle
45
import androidx.activity.ComponentActivity
56
import androidx.activity.compose.setContent
7+
import androidx.activity.enableEdgeToEdge
68
import androidx.compose.foundation.layout.fillMaxSize
79
import androidx.compose.material3.MaterialTheme
810
import androidx.compose.material3.Surface
@@ -51,6 +53,7 @@ sealed class Route : NavKey {
5153
class MainActivity : ComponentActivity() {
5254
override fun onCreate(savedInstanceState: Bundle?) {
5355
super.onCreate(savedInstanceState)
56+
enableEdgeToEdge()
5457

5558
FirebaseApp.initializeApp(applicationContext)
5659
val authUI = FirebaseAuthUI.getInstance()
@@ -62,6 +65,9 @@ class MainActivity : ComponentActivity() {
6265
val configuration = authUIConfiguration {
6366
context = applicationContext
6467
providers {
68+
provider(
69+
AuthProvider.Anonymous
70+
)
6571
provider(
6672
AuthProvider.Email(
6773
isDisplayNameRequired = true,
@@ -102,6 +108,7 @@ class MainActivity : ComponentActivity() {
102108
)
103109
)
104110
}
111+
isAnonymousUpgradeEnabled = true
105112
tosUrl = "https://policies.google.com/terms?hl=en-NG&fg=1"
106113
privacyPolicyUrl = "https://policies.google.com/privacy?hl=en-NG&fg=1"
107114
}
@@ -141,6 +148,7 @@ class MainActivity : ComponentActivity() {
141148

142149
is Route.EmailAuth -> NavEntry(entry) {
143150
LaunchEmailAuth(
151+
context = applicationContext,
144152
authUI = authUI,
145153
configuration = configuration,
146154
backStack = backStack,
@@ -167,55 +175,6 @@ class MainActivity : ComponentActivity() {
167175
}
168176
}
169177

170-
@Composable
171-
private fun LaunchEmailAuth(
172-
authUI: FirebaseAuthUI,
173-
configuration: AuthUIConfiguration,
174-
credentialForLinking: AuthCredential? = null,
175-
backStack: NavBackStack,
176-
emailLink: String? = null
177-
) {
178-
val provider = configuration.providers
179-
.filterIsInstance<AuthProvider.Email>()
180-
.first()
181-
182-
// Handle email link sign-in if present
183-
if (emailLink != null) {
184-
LaunchedEffect(emailLink) {
185-
186-
try {
187-
val emailFromSession =
188-
EmailLinkPersistenceManager
189-
.retrieveSessionRecord(
190-
applicationContext
191-
)?.email
192-
193-
if (emailFromSession != null) {
194-
authUI.signInWithEmailLink(
195-
context = applicationContext,
196-
config = configuration,
197-
provider = provider,
198-
email = emailFromSession,
199-
emailLink = emailLink,
200-
)
201-
}
202-
} catch (e: Exception) {
203-
// Error handling is done via AuthState.Error in the auth flow
204-
}
205-
}
206-
}
207-
208-
EmailAuthMain(
209-
context = applicationContext,
210-
configuration = configuration,
211-
authUI = authUI,
212-
credentialForLinking = credentialForLinking,
213-
onSetupMfa = {
214-
backStack.add(Route.MfaEnrollment)
215-
}
216-
)
217-
}
218-
219178
@Composable
220179
private fun LaunchPhoneAuth(
221180
authUI: FirebaseAuthUI,
@@ -284,4 +243,51 @@ class MainActivity : ComponentActivity() {
284243
backStack.removeLastOrNull()
285244
}
286245
}
246+
}
247+
248+
@Composable
249+
fun LaunchEmailAuth(
250+
context: Context,
251+
authUI: FirebaseAuthUI,
252+
configuration: AuthUIConfiguration,
253+
credentialForLinking: AuthCredential? = null,
254+
backStack: NavBackStack,
255+
emailLink: String? = null
256+
) {
257+
val provider = configuration.providers
258+
.filterIsInstance<AuthProvider.Email>()
259+
.first()
260+
261+
// Handle email link sign-in if present
262+
if (emailLink != null) {
263+
LaunchedEffect(emailLink) {
264+
265+
try {
266+
val emailFromSession =
267+
EmailLinkPersistenceManager.retrieveSessionRecord(context)?.email
268+
269+
if (emailFromSession != null) {
270+
authUI.signInWithEmailLink(
271+
context = context,
272+
config = configuration,
273+
provider = provider,
274+
email = emailFromSession,
275+
emailLink = emailLink,
276+
)
277+
}
278+
} catch (e: Exception) {
279+
// Error handling is done via AuthState.Error in the auth flow
280+
}
281+
}
282+
}
283+
284+
EmailAuthMain(
285+
context = context,
286+
configuration = configuration,
287+
authUI = authUI,
288+
credentialForLinking = credentialForLinking,
289+
onSetupMfa = {
290+
backStack.add(Route.MfaEnrollment)
291+
}
292+
)
287293
}

0 commit comments

Comments
 (0)