Skip to content

Commit 196eca1

Browse files
committed
fix: handle back button cancel and make logoutCallback required in registerCallbacks
1 parent c84ed7a commit 196eca1

File tree

5 files changed

+40
-45
lines changed

5 files changed

+40
-45
lines changed

V4_MIGRATION_GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ class LoginActivity : AppCompatActivity() {
344344
| **Configuration change** (rotation, locale, dark mode) | Any result cached while the Activity was recreating is delivered on the next `onResume` |
345345
| **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 |
346346

347-
> **Note:** `logoutCallback` is optionalpass it only if your screen initiates logout flows.
347+
> **Note:** Both `loginCallback` and `logoutCallback` are requiredthis ensures results from either flow are never lost during configuration changes or process death.
348348
349349
> **Note:** If you use the `suspend fun await()` API from a ViewModel coroutine scope, the
350350
> Activity is never captured in the callback chain, so you do not need `registerCallbacks()` calls.

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.google.androidbrowserhelper.trusted.TwaLauncher
2020
public open class AuthenticationActivity : Activity() {
2121
private var intentLaunched = false
2222
private var customTabsController: CustomTabsController? = null
23+
private var returnedFromBrowser = false
2324
override fun onNewIntent(intent: Intent?) {
2425
super.onNewIntent(intent)
2526
setIntent(intent)
@@ -45,6 +46,13 @@ public open class AuthenticationActivity : Activity() {
4546
}
4647
}
4748

49+
override fun onStop() {
50+
super.onStop()
51+
if (intentLaunched) {
52+
returnedFromBrowser = true
53+
}
54+
}
55+
4856
override fun onResume() {
4957
super.onResume()
5058
val authenticationIntent = intent
@@ -57,11 +65,12 @@ public open class AuthenticationActivity : Activity() {
5765
launchAuthenticationIntent()
5866
return
5967
}
60-
// Only deliver result if intent.data is present (user returned from browser).
61-
// If data is null, the Activity resumed due to rotation or other config change
62-
// while the CustomTab is still open — don't finish, let the browser continue.
63-
val resultMissing = authenticationIntent.data == null
64-
if (!resultMissing) {
68+
val hasResult = authenticationIntent.data != null
69+
if (hasResult || returnedFromBrowser) {
70+
returnedFromBrowser = false
71+
if (!hasResult) {
72+
setResult(RESULT_CANCELED)
73+
}
6574
deliverAuthenticationResult(authenticationIntent)
6675
finish()
6776
}

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,20 @@ public object WebAuthProvider {
5555
internal val pendingLogoutResult = AtomicReference<PendingResult<Void?>?>(null)
5656

5757
/**
58-
* Registers login (and optionally logout) callbacks for the duration of the given
58+
* Registers login and logout callbacks for the duration of the given
5959
* [lifecycleOwner]'s lifetime. Call this once in `onCreate()` — it covers both recovery
6060
* scenarios automatically:
6161
*
62-
* - **Process death**: [loginCallback] is registered immediately so that if the process
63-
* was killed while the browser was open, the result is delivered when the Activity is
64-
* restored. The callback is automatically unregistered when [lifecycleOwner] is destroyed,
62+
* - **Process death**: callbacks are registered immediately so that if the process
63+
* was killed while the browser was open, results are delivered when the Activity is
64+
* restored. Callbacks are automatically unregistered when [lifecycleOwner] is destroyed,
6565
* so there is no need to call [removeCallback] manually.
6666
* - **Configuration change** (rotation, locale, dark mode): any login or logout result
6767
* that arrived while the Activity was being recreated is delivered on the next `onResume`.
6868
*
69+
* Both callbacks are required to ensure results are not lost during configuration changes
70+
* (e.g. user logs out to switch accounts and rotates during the logout flow).
71+
*
6972
* ```kotlin
7073
* override fun onCreate(savedInstanceState: Bundle?) {
7174
* super.onCreate(savedInstanceState)
@@ -81,7 +84,7 @@ public object WebAuthProvider {
8184
public fun registerCallbacks(
8285
lifecycleOwner: LifecycleOwner,
8386
loginCallback: Callback<Credentials, AuthenticationException>,
84-
logoutCallback: Callback<Void?, AuthenticationException>? = null,
87+
logoutCallback: Callback<Void?, AuthenticationException>,
8588
) {
8689
// Process-death recovery: register immediately so result is routed here on restore
8790
callbacks += loginCallback
@@ -95,14 +98,12 @@ public object WebAuthProvider {
9598
}
9699
resetManagerInstance()
97100
}
98-
logoutCallback?.let { cb ->
99-
pendingLogoutResult.getAndSet(null)?.let { pending ->
100-
when (pending) {
101-
is PendingResult.Success -> cb.onSuccess(pending.result)
102-
is PendingResult.Failure -> cb.onFailure(pending.error)
103-
}
104-
resetManagerInstance()
101+
pendingLogoutResult.getAndSet(null)?.let { pending ->
102+
when (pending) {
103+
is PendingResult.Success -> logoutCallback.onSuccess(pending.result)
104+
is PendingResult.Failure -> logoutCallback.onFailure(pending.error)
105105
}
106+
resetManagerInstance()
106107
}
107108
}
108109

@@ -115,7 +116,7 @@ public object WebAuthProvider {
115116

116117
@Deprecated(
117118
message = "Use registerCallbacks() instead — it registers the callback and auto-removes it when the lifecycle owner is destroyed.",
118-
replaceWith = ReplaceWith("registerCallbacks(lifecycleOwner, loginCallback = callback)")
119+
replaceWith = ReplaceWith("registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = logoutCallback)")
119120
)
120121
@JvmStatic
121122
public fun addCallback(callback: Callback<Credentials, AuthenticationException>) {
@@ -124,7 +125,7 @@ public object WebAuthProvider {
124125

125126
@Deprecated(
126127
message = "Use registerCallbacks() instead — it auto-removes the callback when the lifecycle owner is destroyed.",
127-
replaceWith = ReplaceWith("registerCallbacks(lifecycleOwner, loginCallback = callback)")
128+
replaceWith = ReplaceWith("registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = logoutCallback)")
128129
)
129130
@JvmStatic
130131
public fun removeCallback(callback: Callback<Credentials, AuthenticationException>) {

auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,13 @@ public class AuthenticationActivityTest {
214214
MatcherAssert.assertThat(launchAsTwaCaptor.value, Is.`is`(false))
215215
MatcherAssert.assertThat(activity.deliveredIntent, Is.`is`(Matchers.nullValue()))
216216
activityController.pause().stop()
217-
//Browser is shown, resume WITHOUT new intent — should NOT deliver or finish
218217
activityController.start().resume()
219-
MatcherAssert.assertThat(activity.deliveredIntent, Is.`is`(Matchers.nullValue())) //nothing delivered
220-
MatcherAssert.assertThat(activity.isFinishing, Is.`is`(false)) //still waiting for result
218+
MatcherAssert.assertThat(activity.deliveredIntent, Is.`is`(Matchers.notNullValue()))
219+
MatcherAssert.assertThat(
220+
activity.deliveredIntent!!.data,
221+
Is.`is`(Matchers.nullValue())
222+
)
223+
MatcherAssert.assertThat(activity.isFinishing, Is.`is`(true))
221224
activityController.destroy()
222225
Mockito.verify(customTabsController).unbindService()
223226
}

auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3302,7 +3302,7 @@ public class WebAuthProviderTest {
33023302
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33033303

33043304
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3305-
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3305+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
33063306
verify(lifecycle).addObserver(observerCaptor.capture())
33073307

33083308
// Pending result is delivered on onResume, not immediately
@@ -3323,7 +3323,7 @@ public class WebAuthProviderTest {
33233323
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33243324

33253325
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3326-
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3326+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
33273327
verify(lifecycle).addObserver(observerCaptor.capture())
33283328

33293329
observerCaptor.firstValue.onResume(lifecycleOwner)
@@ -3375,7 +3375,7 @@ public class WebAuthProviderTest {
33753375
val lifecycle = Mockito.mock(Lifecycle::class.java)
33763376
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33773377

3378-
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3378+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
33793379

33803380
Assert.assertTrue(WebAuthProvider.callbacks.contains(callback))
33813381
}
@@ -3387,7 +3387,7 @@ public class WebAuthProviderTest {
33873387
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33883388

33893389
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3390-
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3390+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
33913391
verify(lifecycle).addObserver(observerCaptor.capture())
33923392

33933393
Assert.assertTrue(WebAuthProvider.callbacks.contains(callback))
@@ -3398,24 +3398,6 @@ public class WebAuthProviderTest {
33983398
Assert.assertFalse(WebAuthProvider.callbacks.contains(callback))
33993399
}
34003400

3401-
@Test
3402-
public fun shouldNotDeliverLogoutResultWhenNoLogoutCallbackPassedToRegisterCallbacks() {
3403-
setPendingLogoutResult(WebAuthProvider.PendingResult.Success(null))
3404-
3405-
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
3406-
val lifecycle = Mockito.mock(Lifecycle::class.java)
3407-
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
3408-
3409-
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3410-
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3411-
verify(lifecycle).addObserver(observerCaptor.capture())
3412-
3413-
// registerCallbacks without logoutCallback — pending logout result should stay untouched
3414-
observerCaptor.firstValue.onResume(lifecycleOwner)
3415-
3416-
verify(voidCallback, Mockito.never()).onSuccess(any())
3417-
Assert.assertNotNull(getPendingLogoutResult())
3418-
}
34193401

34203402
// Direct access — pendingLoginResult/pendingLogoutResult are internal in WebAuthProvider
34213403
private fun setPendingLoginResult(result: WebAuthProvider.PendingResult<Credentials>?) {

0 commit comments

Comments
 (0)