Skip to content

Commit 194a896

Browse files
authored
Merge branch 'v4_development' into SDK-8576
2 parents 464aefa + 6a6c95d commit 194a896

7 files changed

Lines changed: 704 additions & 10 deletions

File tree

EXAMPLES.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,59 @@ WebAuthProvider.logout(account)
322322

323323
})
324324
```
325-
> [!NOTE]
325+
> [!NOTE]
326326
> DPoP is supported only on Android version 6.0 (API level 23) and above. Trying to use DPoP in any older versions will result in an exception.
327327
328+
## Handling Configuration Changes During Authentication
329+
330+
When the Activity is destroyed during authentication due to a configuration change (e.g. device rotation, locale change, dark mode toggle), the SDK caches the authentication result internally. Call `WebAuthProvider.registerCallbacks()` once in your `onCreate()` to recover it. This single call handles both recovery scenarios:
331+
332+
- **Configuration change**: delivers any cached result on the next `onResume` to the callback
333+
- **Process death**: `AuthenticationActivity` restores OAuth state and processes the redirect. Since static state was wiped, the result is cached and delivered to `loginCallback` on the next `onResume` after `registerCallbacks()` is called
334+
335+
```kotlin
336+
class LoginActivity : AppCompatActivity() {
337+
338+
override fun onCreate(savedInstanceState: Bundle?) {
339+
super.onCreate(savedInstanceState)
340+
WebAuthProvider.registerCallbacks(
341+
lifecycleOwner = this,
342+
loginCallback = object : Callback<Credentials, AuthenticationException> {
343+
override fun onSuccess(result: Credentials) {
344+
// Handle successful login
345+
}
346+
override fun onFailure(error: AuthenticationException) {
347+
// Handle error
348+
}
349+
},
350+
logoutCallback = object : Callback<Void?, AuthenticationException> {
351+
override fun onSuccess(result: Void?) {
352+
// Handle successful logout
353+
}
354+
override fun onFailure(error: AuthenticationException) {
355+
// Handle error
356+
}
357+
}
358+
)
359+
}
360+
361+
fun onLoginClick() {
362+
WebAuthProvider.login(account)
363+
.withScheme("demo")
364+
.start(this, loginCallback)
365+
}
366+
367+
fun onLogoutClick() {
368+
WebAuthProvider.logout(account)
369+
.withScheme("demo")
370+
.start(this, logoutCallback)
371+
}
372+
}
373+
```
374+
375+
> [!NOTE]
376+
> If you use the `suspend fun await()` API from a ViewModel coroutine scope, the Activity is never captured in the callback chain, so you do not need `registerCallbacks()` calls.
377+
328378
## Authentication API
329379

330380
The client provides methods to authenticate the user against the Auth0 server.

V4_MIGRATION_GUIDE.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ v4 of the Auth0 Android SDK includes significant build toolchain updates, update
2727
- [**Dependency Changes**](#dependency-changes)
2828
+ [Gson 2.8.9 → 2.11.0](#️-gson-289--2110-transitive-dependency)
2929
+ [DefaultClient.Builder](#defaultclientbuilder)
30+
- [**New APIs**](#new-apis)
31+
+ [Handling Configuration Changes During Authentication](#handling-configuration-changes-during-authentication)
3032

3133
---
3234

@@ -317,6 +319,59 @@ The legacy constructor is deprecated but **not removed** — existing code will
317319
and run. Your IDE will show a deprecation warning with a suggested `ReplaceWith` quick-fix to
318320
migrate to the Builder.
319321

322+
## New APIs
323+
324+
### Handling Configuration Changes During Authentication
325+
326+
v4 fixes a memory leak and lost callback issue when the Activity is destroyed during authentication
327+
(e.g. device rotation, locale change, dark mode toggle). The SDK wraps the callback in a
328+
`LifecycleAwareCallback` that observes the host Activity/Fragment lifecycle. When `onDestroy` fires,
329+
the reference to the callback is immediately nulled out so the destroyed Activity is no longer held
330+
in memory.
331+
332+
If the authentication result arrives while the Activity is being recreated, it is cached internally.
333+
Call `WebAuthProvider.registerCallbacks()` once in your `onCreate()` — this single call handles both
334+
recovery scenarios and manages the callback lifecycle automatically:
335+
336+
```kotlin
337+
class LoginActivity : AppCompatActivity() {
338+
339+
override fun onCreate(savedInstanceState: Bundle?) {
340+
super.onCreate(savedInstanceState)
341+
WebAuthProvider.registerCallbacks(
342+
lifecycleOwner = this,
343+
loginCallback = object : Callback<Credentials, AuthenticationException> {
344+
override fun onSuccess(result: Credentials) { /* handle credentials */ }
345+
override fun onFailure(error: AuthenticationException) { /* handle error */ }
346+
},
347+
logoutCallback = object : Callback<Void?, AuthenticationException> {
348+
override fun onSuccess(result: Void?) { /* handle logout */ }
349+
override fun onFailure(error: AuthenticationException) { /* handle error */ }
350+
}
351+
)
352+
}
353+
354+
fun onLoginClick() {
355+
WebAuthProvider.login(account)
356+
.withScheme("myapp")
357+
.start(this, callback)
358+
}
359+
}
360+
```
361+
362+
`registerCallbacks()` covers both scenarios in one call:
363+
364+
| Scenario | How it's handled |
365+
|----------|-----------------|
366+
| **Configuration change** (rotation, locale, dark mode) | The result is delivered directly to the registered callback once the async token exchange completes. If no callback is registered yet, the result is cached and delivered on the next `onResume` |
367+
| **Process death** (system killed the app while browser was open) | `AuthenticationActivity` restores the OAuth state and processes the redirect. Since all static state (including `callbacks`) was wiped, the result is cached in `pendingLoginResult`. When your Activity is recreated and calls `registerCallbacks()`, the cached result is delivered on the next `onResume` |
368+
369+
> **Note:** Both `loginCallback` and `logoutCallback` are required — this ensures results from either flow are never lost during configuration changes or process death.
370+
371+
> **Note:** If you use the `suspend fun await()` API from a ViewModel coroutine scope, the
372+
> Activity is never captured in the callback chain, so you do not need `registerCallbacks()` calls.
373+
> See the sample app for a ViewModel-based example.
374+
320375
## Getting Help
321376

322377
If you encounter issues during migration:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.auth0.android.provider
2+
3+
import androidx.lifecycle.DefaultLifecycleObserver
4+
import androidx.lifecycle.LifecycleOwner
5+
import com.auth0.android.authentication.AuthenticationException
6+
import com.auth0.android.callback.Callback
7+
8+
/**
9+
* Wraps a user-provided callback and observes the Activity/Fragment lifecycle.
10+
* When the host is destroyed (e.g. config change), [delegateCallback] is set to null so
11+
* the destroyed Activity is no longer referenced by the SDK.
12+
*
13+
* If a result arrives after [delegateCallback] has been cleared, the [onDetached] lambda
14+
* is invoked to cache the result for later recovery via resumePending*Result().
15+
*
16+
* @param S the success type (Credentials for login, Void? for logout)
17+
* @param delegateCallback the user's original callback
18+
* @param lifecycleOwner the Activity or Fragment whose lifecycle to observe
19+
* @param onDetached called when a result arrives but the callback is already detached
20+
*/
21+
internal class LifecycleAwareCallback<S>(
22+
@Volatile private var delegateCallback: Callback<S, AuthenticationException>?,
23+
lifecycleOwner: LifecycleOwner,
24+
private val onDetached: (success: S?, error: AuthenticationException?) -> Unit,
25+
) : Callback<S, AuthenticationException>, DefaultLifecycleObserver {
26+
27+
init {
28+
lifecycleOwner.lifecycle.addObserver(this)
29+
}
30+
31+
override fun onSuccess(result: S) {
32+
val cb = delegateCallback
33+
if (cb != null) {
34+
cb.onSuccess(result)
35+
} else {
36+
onDetached(result, null)
37+
}
38+
}
39+
40+
override fun onFailure(error: AuthenticationException) {
41+
val cb = delegateCallback
42+
if (cb != null) {
43+
cb.onFailure(error)
44+
} else {
45+
onDetached(null, error)
46+
}
47+
}
48+
49+
override fun onDestroy(owner: LifecycleOwner) {
50+
delegateCallback = null
51+
owner.lifecycle.removeObserver(this)
52+
}
53+
}

auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal class OAuthManager(
2929
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
3030
internal val dPoP: DPoP? = null
3131
) : ResumableManager() {
32+
3233
private val parameters: MutableMap<String, String>
3334
private val headers: MutableMap<String, String>
3435
private val ctOptions: CustomTabsOptions

0 commit comments

Comments
 (0)