Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -57,12 +57,14 @@ public open class AuthenticationActivity : Activity() {
launchAuthenticationIntent()
return
}
// Only deliver result if intent.data is present (user returned from browser).
// If data is null, the Activity resumed due to rotation or other config change
// while the CustomTab is still open — don't finish, let the browser continue.
val resultMissing = authenticationIntent.data == null
if (resultMissing) {
setResult(RESULT_CANCELED)
if (!resultMissing) {
Copy link
Copy Markdown
Contributor

@pmathew92 pmathew92 Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will this change behave when user back presses the browser ? With this change, the AuthenticationActivity stays in an invisible state doing nothing and user might get stuck

deliverAuthenticationResult(authenticationIntent)
finish()
}
deliverAuthenticationResult(authenticationIntent)
finish()
}

override fun onDestroy() {
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>(
@Volatile 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