Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b6cacb3
fix: handle configuration changes during WebAuth flow to prevent memo…
utkrishtsahu Mar 24, 2026
8058fe8
fix: handle configuration changes during WebAuth flow to prevent memo…
utkrishtsahu Mar 31, 2026
8795d30
Merge branch 'v4_development' into SDK-6233-The-SDK-doesn-t-handle-co…
utkrishtsahu Mar 31, 2026
c0831d1
updated Migration doc
utkrishtsahu Mar 31, 2026
61e92fe
fix: handle config changes in WebAuth flow with unified attach() API
utkrishtsahu Apr 2, 2026
6ed2d1f
updated example.md file
utkrishtsahu Apr 2, 2026
6b7675c
refactor: rename attach() to registerCallbacks(), deliver pending res…
utkrishtsahu Apr 2, 2026
ad0df64
make pendingLoginResult and pendingLogoutResult private, use reflecti…
utkrishtsahu Apr 2, 2026
ec02d07
fix: cache result in onRestoreInstanceState for process death recover…
utkrishtsahu Apr 2, 2026
0886953
fix: prevent rotation from canceling auth, clear stale pending result…
utkrishtsahu Apr 2, 2026
c84ed7a
Fixing UT case
utkrishtsahu Apr 2, 2026
196eca1
fix: handle back button cancel and make logoutCallback required in re…
utkrishtsahu Apr 7, 2026
d063324
fix: deliver auth result directly to registered callbacks when token …
utkrishtsahu Apr 8, 2026
ebfed96
fix: broadcast auth result directly to registered callbacks on async …
utkrishtsahu Apr 8, 2026
43d43ca
Handling review comments
utkrishtsahu Apr 9, 2026
5eba7ac
Merge remote-tracking branch 'origin/v4_development' into SDK-6233-Th…
utkrishtsahu Apr 9, 2026
b01245f
fix: address review comments — add @VisibleForTesting annotations, fi…
utkrishtsahu Apr 9, 2026
21a8532
Updtaed UT cases as per review comment
utkrishtsahu Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,59 @@ WebAuthProvider.logout(account)

})
```
> [!NOTE]
> [!NOTE]
> 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.

## Handling Configuration Changes During Authentication

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:

- **Configuration change**: delivers any cached result on the next `onResume` to the callback
- **Process death**: registers `loginCallback` as a listener and auto-removes it when the Activity is destroyed

```kotlin
class LoginActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WebAuthProvider.registerCallbacks(
lifecycleOwner = this,
loginCallback = object : Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
// Handle successful login
}
override fun onFailure(error: AuthenticationException) {
// Handle error
}
},
logoutCallback = object : Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) {
// Handle successful logout
}
override fun onFailure(error: AuthenticationException) {
// Handle error
}
}
)
}

fun onLoginClick() {
WebAuthProvider.login(account)
.withScheme("demo")
.start(this, loginCallback)
}

fun onLogoutClick() {
WebAuthProvider.logout(account)
.withScheme("demo")
.start(this, logoutCallback)
}
}
```

> [!NOTE]
> 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.

## Authentication API

The client provides methods to authenticate the user against the Auth0 server.
Expand Down
55 changes: 55 additions & 0 deletions V4_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ v4 of the Auth0 Android SDK includes significant build toolchain updates, update
- [**Dependency Changes**](#dependency-changes)
+ [Gson 2.8.9 → 2.11.0](#️-gson-289--2110-transitive-dependency)
+ [DefaultClient.Builder](#defaultclientbuilder)
- [**New APIs**](#new-apis)
+ [Handling Configuration Changes During Authentication](#handling-configuration-changes-during-authentication)

---

Expand Down Expand Up @@ -295,6 +297,59 @@ The legacy constructor is deprecated but **not removed** — existing code will
and run. Your IDE will show a deprecation warning with a suggested `ReplaceWith` quick-fix to
migrate to the Builder.

## New APIs

### Handling Configuration Changes During Authentication

v4 fixes a memory leak and lost callback issue when the Activity is destroyed during authentication
(e.g. device rotation, locale change, dark mode toggle). The SDK wraps the callback in a
`LifecycleAwareCallback` that observes the host Activity/Fragment lifecycle. When `onDestroy` fires,
the reference to the callback is immediately nulled out so the destroyed Activity is no longer held
in memory.

If the authentication result arrives while the Activity is being recreated, it is cached internally.
Call `WebAuthProvider.registerCallbacks()` once in your `onCreate()` — this single call handles both
recovery scenarios and manages the callback lifecycle automatically:

```kotlin
class LoginActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WebAuthProvider.registerCallbacks(
lifecycleOwner = this,
loginCallback = object : Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) { /* handle credentials */ }
override fun onFailure(error: AuthenticationException) { /* handle error */ }
},
logoutCallback = object : Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) { /* handle logout */ }
override fun onFailure(error: AuthenticationException) { /* handle error */ }
}
)
}

fun onLoginClick() {
WebAuthProvider.login(account)
.withScheme("myapp")
.start(this, callback)
}
}
```

`registerCallbacks()` covers both scenarios in one call:

| Scenario | How it's handled |
|----------|-----------------|
| **Configuration change** (rotation, locale, dark mode) | Any result cached while the Activity was recreating is delivered on the next `onResume` |
| **Process death** (system killed the app while browser was open) | `loginCallback` is registered as a listener and auto-removed when `lifecycleOwner` is destroyed — no manual `addCallback`/`removeCallback` calls needed |

> **Note:** `logoutCallback` is optional — pass it only if your screen initiates logout flows.

> **Note:** 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.
> See the sample app for a ViewModel-based example.

## Getting Help

If you encounter issues during migration:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.auth0.android.provider

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback

/**
* Wraps a user-provided callback and observes the Activity/Fragment lifecycle.
* When the host is destroyed (e.g. config change), [delegateCallback] is set to null so
* the destroyed Activity is no longer referenced by the SDK.
*
* If a result arrives after [delegateCallback] has been cleared, the [onDetached] lambda
* is invoked to cache the result for later recovery via resumePending*Result().
*
* @param S the success type (Credentials for login, Void? for logout)
* @param delegateCallback the user's original callback
* @param lifecycleOwner the Activity or Fragment whose lifecycle to observe
* @param onDetached called when a result arrives but the callback is already detached
*/
internal class LifecycleAwareCallback<S>(
private var delegateCallback: Callback<S, AuthenticationException>?,
lifecycleOwner: LifecycleOwner,
private val onDetached: (success: S?, error: AuthenticationException?) -> Unit,
) : Callback<S, AuthenticationException>, DefaultLifecycleObserver {

init {
lifecycleOwner.lifecycle.addObserver(this)
}

override fun onSuccess(result: S) {
val cb = delegateCallback
if (cb != null) {
cb.onSuccess(result)
} else {
onDetached(result, null)
}
}

override fun onFailure(error: AuthenticationException) {
val cb = delegateCallback
if (cb != null) {
cb.onFailure(error)
} else {
onDetached(null, error)
}
}

override fun onDestroy(owner: LifecycleOwner) {
delegateCallback = null
owner.lifecycle.removeObserver(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal class OAuthManager(
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val dPoP: DPoP? = null
) : ResumableManager() {

private val parameters: MutableMap<String, String>
private val headers: MutableMap<String, String>
private val ctOptions: CustomTabsOptions
Expand Down
Loading
Loading