Skip to content

Commit 6b7675c

Browse files
committed
refactor: rename attach() to registerCallbacks(), deliver pending results on onResume, and make LogoutBuilder.startInternal private
1 parent 6ed2d1f commit 6b7675c

4 files changed

Lines changed: 85 additions & 63 deletions

File tree

EXAMPLES.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,17 +327,17 @@ WebAuthProvider.logout(account)
327327
328328
## Handling Configuration Changes During Authentication
329329

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.attach()` in your `onResume()` to recover it. This single call handles both recovery scenarios:
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:
331331

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

335335
```kotlin
336336
class LoginActivity : AppCompatActivity() {
337337

338-
override fun onResume() {
339-
super.onResume()
340-
WebAuthProvider.attach(
338+
override fun onCreate(savedInstanceState: Bundle?) {
339+
super.onCreate(savedInstanceState)
340+
WebAuthProvider.registerCallbacks(
341341
lifecycleOwner = this,
342342
loginCallback = object : Callback<Credentials, AuthenticationException> {
343343
override fun onSuccess(result: Credentials) {
@@ -373,7 +373,7 @@ class LoginActivity : AppCompatActivity() {
373373
```
374374

375375
> [!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 `attach()` calls.
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.
377377
378378
## Authentication API
379379

V4_MIGRATION_GUIDE.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -308,15 +308,15 @@ the reference to the callback is immediately nulled out so the destroyed Activit
308308
in memory.
309309

310310
If the authentication result arrives while the Activity is being recreated, it is cached internally.
311-
Use `WebAuthProvider.attach()` in your `onResume()` to recover it — this single call handles both
311+
Call `WebAuthProvider.registerCallbacks()` once in your `onCreate()` — this single call handles both
312312
recovery scenarios and manages the callback lifecycle automatically:
313313

314314
```kotlin
315315
class LoginActivity : AppCompatActivity() {
316316

317-
override fun onResume() {
318-
super.onResume()
319-
WebAuthProvider.attach(
317+
override fun onCreate(savedInstanceState: Bundle?) {
318+
super.onCreate(savedInstanceState)
319+
WebAuthProvider.registerCallbacks(
320320
lifecycleOwner = this,
321321
loginCallback = object : Callback<Credentials, AuthenticationException> {
322322
override fun onSuccess(result: Credentials) { /* handle credentials */ }
@@ -337,17 +337,17 @@ class LoginActivity : AppCompatActivity() {
337337
}
338338
```
339339

340-
`attach()` covers both scenarios in one call:
340+
`registerCallbacks()` covers both scenarios in one call:
341341

342342
| Scenario | How it's handled |
343343
|----------|-----------------|
344-
| **Configuration change** (rotation, locale, dark mode) | Any result cached while the Activity was recreating is delivered immediately to the callback |
344+
| **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

347347
> **Note:** `logoutCallback` is optional — pass it only if your screen initiates logout flows.
348348
349349
> **Note:** If you use the `suspend fun await()` API from a ViewModel coroutine scope, the
350-
> Activity is never captured in the callback chain, so you do not need `attach()` calls.
350+
> Activity is never captured in the callback chain, so you do not need `registerCallbacks()` calls.
351351
> See the sample app for a ViewModel-based example.
352352
353353
## Getting Help

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

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

5757
/**
58-
* Attaches login (and optionally logout) callbacks for the duration of the given
59-
* [lifecycleOwner]'s lifetime. Call this once in `onResume()` — it covers both recovery
58+
* Registers login (and optionally logout) callbacks for the duration of the given
59+
* [lifecycleOwner]'s lifetime. Call this once in `onCreate()` — it covers both recovery
6060
* scenarios automatically:
6161
*
62-
* - **Configuration change** (rotation, locale, dark mode): if a login or logout result
63-
* arrived while the Activity was being recreated, it is delivered immediately.
64-
* - **Process death**: [loginCallback] is registered as a listener so that if the process
62+
* - **Process death**: [loginCallback] is registered immediately so that if the process
6563
* was killed while the browser was open, the result is delivered when the Activity is
6664
* restored. The callback is automatically unregistered when [lifecycleOwner] is destroyed,
6765
* so there is no need to call [removeCallback] manually.
66+
* - **Configuration change** (rotation, locale, dark mode): any login or logout result
67+
* that arrived while the Activity was being recreated is delivered on the next `onResume`.
6868
*
6969
* ```kotlin
70-
* override fun onResume() {
71-
* super.onResume()
72-
* WebAuthProvider.attach(this, loginCallback = callback, logoutCallback = voidCallback)
70+
* override fun onCreate(savedInstanceState: Bundle?) {
71+
* super.onCreate(savedInstanceState)
72+
* WebAuthProvider.registerCallbacks(this, loginCallback = callback, logoutCallback = voidCallback)
7373
* }
7474
* ```
7575
*
@@ -78,52 +78,53 @@ public object WebAuthProvider {
7878
* @param logoutCallback receives logout results recovered after a configuration change
7979
*/
8080
@JvmStatic
81-
public fun attach(
81+
public fun registerCallbacks(
8282
lifecycleOwner: LifecycleOwner,
8383
loginCallback: Callback<Credentials, AuthenticationException>,
8484
logoutCallback: Callback<Void?, AuthenticationException>? = null,
8585
) {
86-
// Process-death recovery: register and auto-remove on destroy
86+
// Process-death recovery: register immediately so result is routed here on restore
8787
callbacks += loginCallback
8888
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
89+
override fun onResume(owner: LifecycleOwner) {
90+
// Config-change recovery: deliver any result cached while Activity was recreating
91+
pendingLoginResult.getAndSet(null)?.let { pending ->
92+
when (pending) {
93+
is PendingResult.Success -> loginCallback.onSuccess(pending.result)
94+
is PendingResult.Failure -> loginCallback.onFailure(pending.error)
95+
}
96+
resetManagerInstance()
97+
}
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()
105+
}
106+
}
107+
}
108+
89109
override fun onDestroy(owner: LifecycleOwner) {
90110
callbacks -= loginCallback
91111
owner.lifecycle.removeObserver(this)
92112
}
93113
})
94-
95-
// Config-change recovery: deliver any result cached while Activity was recreating
96-
pendingLoginResult.getAndSet(null)?.let { pending ->
97-
when (pending) {
98-
is PendingResult.Success -> loginCallback.onSuccess(pending.result)
99-
is PendingResult.Failure -> loginCallback.onFailure(pending.error)
100-
}
101-
resetManagerInstance()
102-
}
103-
104-
logoutCallback?.let { cb ->
105-
pendingLogoutResult.getAndSet(null)?.let { pending ->
106-
when (pending) {
107-
is PendingResult.Success -> cb.onSuccess(pending.result)
108-
is PendingResult.Failure -> cb.onFailure(pending.error)
109-
}
110-
resetManagerInstance()
111-
}
112-
}
113114
}
114115

115116
@Deprecated(
116-
message = "Use attach() instead — it registers the callback and auto-removes it when the lifecycle owner is destroyed.",
117-
replaceWith = ReplaceWith("attach(lifecycleOwner, loginCallback = callback)")
117+
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)")
118119
)
119120
@JvmStatic
120121
public fun addCallback(callback: Callback<Credentials, AuthenticationException>) {
121122
callbacks += callback
122123
}
123124

124125
@Deprecated(
125-
message = "Use attach() instead — it auto-removes the callback when the lifecycle owner is destroyed.",
126-
replaceWith = ReplaceWith("attach(lifecycleOwner, loginCallback = callback)")
126+
message = "Use registerCallbacks() instead — it auto-removes the callback when the lifecycle owner is destroyed.",
127+
replaceWith = ReplaceWith("registerCallbacks(lifecycleOwner, loginCallback = callback)")
127128
)
128129
@JvmStatic
129130
public fun removeCallback(callback: Callback<Credentials, AuthenticationException>) {
@@ -342,7 +343,7 @@ public object WebAuthProvider {
342343
startInternal(context, effectiveCallback)
343344
}
344345

345-
internal fun startInternal(context: Context, callback: Callback<Void?, AuthenticationException>) {
346+
private fun startInternal(context: Context, callback: Callback<Void?, AuthenticationException>) {
346347
resetManagerInstance()
347348
if (!ctOptions.hasCompatibleBrowser(context.packageManager)) {
348349
val ex = AuthenticationException(

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

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3300,84 +3300,101 @@ public class WebAuthProviderTest {
33003300

33013301

33023302
@Test
3303-
public fun shouldDeliverPendingLoginResultOnAttach() {
3303+
public fun shouldDeliverPendingLoginResultOnResume() {
33043304
val credentials = Mockito.mock(Credentials::class.java)
33053305
WebAuthProvider.pendingLoginResult.set(WebAuthProvider.PendingResult.Success(credentials))
33063306

33073307
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
33083308
val lifecycle = Mockito.mock(Lifecycle::class.java)
33093309
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33103310

3311-
WebAuthProvider.attach(lifecycleOwner, loginCallback = callback)
3311+
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3312+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3313+
verify(lifecycle).addObserver(observerCaptor.capture())
3314+
3315+
// Pending result is delivered on onResume, not immediately
3316+
verify(callback, Mockito.never()).onSuccess(any())
3317+
observerCaptor.firstValue.onResume(lifecycleOwner)
33123318

33133319
verify(callback).onSuccess(credentials)
33143320
Assert.assertNull(WebAuthProvider.pendingLoginResult.get())
33153321
}
33163322

33173323
@Test
3318-
public fun shouldDeliverPendingLoginFailureOnAttach() {
3324+
public fun shouldDeliverPendingLoginFailureOnResume() {
33193325
val error = AuthenticationException("canceled", "User canceled")
33203326
WebAuthProvider.pendingLoginResult.set(WebAuthProvider.PendingResult.Failure(error))
33213327

33223328
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
33233329
val lifecycle = Mockito.mock(Lifecycle::class.java)
33243330
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33253331

3326-
WebAuthProvider.attach(lifecycleOwner, loginCallback = callback)
3332+
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3333+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3334+
verify(lifecycle).addObserver(observerCaptor.capture())
3335+
3336+
observerCaptor.firstValue.onResume(lifecycleOwner)
33273337

33283338
verify(callback).onFailure(error)
33293339
Assert.assertNull(WebAuthProvider.pendingLoginResult.get())
33303340
}
33313341

33323342
@Test
3333-
public fun shouldDeliverPendingLogoutResultOnAttach() {
3343+
public fun shouldDeliverPendingLogoutResultOnResume() {
33343344
WebAuthProvider.pendingLogoutResult.set(WebAuthProvider.PendingResult.Success(null))
33353345

33363346
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
33373347
val lifecycle = Mockito.mock(Lifecycle::class.java)
33383348
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33393349

3340-
WebAuthProvider.attach(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
3350+
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3351+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
3352+
verify(lifecycle).addObserver(observerCaptor.capture())
3353+
3354+
observerCaptor.firstValue.onResume(lifecycleOwner)
33413355

33423356
verify(voidCallback).onSuccess(null)
33433357
Assert.assertNull(WebAuthProvider.pendingLogoutResult.get())
33443358
}
33453359

33463360
@Test
3347-
public fun shouldDeliverPendingLogoutFailureOnAttach() {
3361+
public fun shouldDeliverPendingLogoutFailureOnResume() {
33483362
val error = AuthenticationException("canceled", "User closed the browser")
33493363
WebAuthProvider.pendingLogoutResult.set(WebAuthProvider.PendingResult.Failure(error))
33503364

33513365
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
33523366
val lifecycle = Mockito.mock(Lifecycle::class.java)
33533367
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33543368

3355-
WebAuthProvider.attach(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
3369+
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3370+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback, logoutCallback = voidCallback)
3371+
verify(lifecycle).addObserver(observerCaptor.capture())
3372+
3373+
observerCaptor.firstValue.onResume(lifecycleOwner)
33563374

33573375
verify(voidCallback).onFailure(error)
33583376
Assert.assertNull(WebAuthProvider.pendingLogoutResult.get())
33593377
}
33603378

33613379
@Test
3362-
public fun shouldRegisterLoginCallbackForProcessDeathOnAttach() {
3380+
public fun shouldRegisterLoginCallbackForProcessDeathOnRegisterCallbacks() {
33633381
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
33643382
val lifecycle = Mockito.mock(Lifecycle::class.java)
33653383
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33663384

3367-
WebAuthProvider.attach(lifecycleOwner, loginCallback = callback)
3385+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
33683386

33693387
Assert.assertTrue(WebAuthProvider.callbacks.contains(callback))
33703388
}
33713389

33723390
@Test
3373-
public fun shouldAutoRemoveLoginCallbackOnDestroyAfterAttach() {
3391+
public fun shouldAutoRemoveLoginCallbackOnDestroyAfterRegisterCallbacks() {
33743392
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
33753393
val lifecycle = Mockito.mock(Lifecycle::class.java)
33763394
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33773395

3378-
// Capture the observer registered by attach()
33793396
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3380-
WebAuthProvider.attach(lifecycleOwner, loginCallback = callback)
3397+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
33813398
verify(lifecycle).addObserver(observerCaptor.capture())
33823399

33833400
Assert.assertTrue(WebAuthProvider.callbacks.contains(callback))
@@ -3389,15 +3406,19 @@ public class WebAuthProviderTest {
33893406
}
33903407

33913408
@Test
3392-
public fun shouldNotDeliverLogoutResultWhenNoLogoutCallbackPassedToAttach() {
3409+
public fun shouldNotDeliverLogoutResultWhenNoLogoutCallbackPassedToRegisterCallbacks() {
33933410
WebAuthProvider.pendingLogoutResult.set(WebAuthProvider.PendingResult.Success(null))
33943411

33953412
val lifecycleOwner = Mockito.mock(LifecycleOwner::class.java)
33963413
val lifecycle = Mockito.mock(Lifecycle::class.java)
33973414
Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle)
33983415

3399-
// attach without logoutCallback — pending logout result should stay untouched
3400-
WebAuthProvider.attach(lifecycleOwner, loginCallback = callback)
3416+
val observerCaptor = argumentCaptor<DefaultLifecycleObserver>()
3417+
WebAuthProvider.registerCallbacks(lifecycleOwner, loginCallback = callback)
3418+
verify(lifecycle).addObserver(observerCaptor.capture())
3419+
3420+
// registerCallbacks without logoutCallback — pending logout result should stay untouched
3421+
observerCaptor.firstValue.onResume(lifecycleOwner)
34013422

34023423
verify(voidCallback, Mockito.never()).onSuccess(any())
34033424
Assert.assertNotNull(WebAuthProvider.pendingLogoutResult.get())

0 commit comments

Comments
 (0)