@@ -5,7 +5,7 @@ import android.content.Intent
55import android.net.Uri
66import android.os.Bundle
77import android.util.Log
8- import androidx.annotation.VisibleForTesting
8+ import androidx.lifecycle.DefaultLifecycleObserver
99import androidx.lifecycle.LifecycleOwner
1010import com.auth0.android.Auth0
1111import com.auth0.android.annotation.ExperimentalAuth0Api
@@ -34,10 +34,9 @@ public object WebAuthProvider {
3434 private val TAG : String? = WebAuthProvider ::class .simpleName
3535 private const val KEY_BUNDLE_OAUTH_MANAGER_STATE = " oauth_manager_state"
3636
37- private val callbacks = CopyOnWriteArraySet <Callback <Credentials , AuthenticationException >>()
37+ internal val callbacks = CopyOnWriteArraySet <Callback <Credentials , AuthenticationException >>()
3838
3939 @JvmStatic
40- @get:VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
4140 internal var managerInstance: ResumableManager ? = null
4241 private set
4342
@@ -46,62 +45,86 @@ public object WebAuthProvider {
4645 * the original callback was no longer reachable (e.g. Activity destroyed
4746 * during a configuration change).
4847 */
49- internal sealed class PendingResult <out S , out E > {
50- data class Success <S >(val result : S ) : PendingResult<S, Nothing >()
51- data class Failure < E > (val error : E ) : PendingResult<Nothing, E >()
48+ internal sealed class PendingResult <out S > {
49+ data class Success <S >(val result : S ) : PendingResult<S>()
50+ data class Failure (val error : AuthenticationException ) : PendingResult<Nothing>()
5251 }
5352
54- @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
55- internal val pendingLoginResult =
56- AtomicReference <PendingResult <Credentials , AuthenticationException >? > (null )
53+ internal val pendingLoginResult = AtomicReference <PendingResult <Credentials >? > (null )
5754
58- @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
59- internal val pendingLogoutResult =
60- AtomicReference <PendingResult <Void ?, AuthenticationException >? > (null )
55+ internal val pendingLogoutResult = AtomicReference <PendingResult <Void ?>? > (null )
6156
6257 /* *
63- * Check for and consume a pending login result that arrived during a configuration change.
64- * Call this in your Activity's `onResume()` to recover results that were delivered while the
65- * Activity was being recreated (e.g. due to screen rotation).
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
60+ * scenarios automatically:
6661 *
67- * @param callback the callback to deliver the pending result to
68- * @return true if a pending result was found and delivered, false otherwise
69- */
70- @JvmStatic
71- public fun consumePendingLoginResult (callback : Callback <Credentials , AuthenticationException >): Boolean {
72- val result = pendingLoginResult.getAndSet(null ) ? : return false
73- when (result) {
74- is PendingResult .Success -> callback.onSuccess(result.result)
75- is PendingResult .Failure -> callback.onFailure(result.error)
76- }
77- resetManagerInstance()
78- return true
79- }
80-
81- /* *
82- * Check for and consume a pending logout result that arrived during a configuration change.
83- * Call this in your Activity's `onResume()` to recover results that were delivered while the
84- * Activity was being recreated (e.g. due to screen rotation).
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
65+ * was killed while the browser was open, the result is delivered when the Activity is
66+ * restored. The callback is automatically unregistered when [lifecycleOwner] is destroyed,
67+ * so there is no need to call [removeCallback] manually.
68+ *
69+ * ```kotlin
70+ * override fun onResume() {
71+ * super.onResume()
72+ * WebAuthProvider.attach(this, loginCallback = callback, logoutCallback = voidCallback)
73+ * }
74+ * ```
8575 *
86- * @param callback the callback to deliver the pending result to
87- * @return true if a pending result was found and delivered, false otherwise
76+ * @param lifecycleOwner the Activity or Fragment whose lifecycle to observe
77+ * @param loginCallback receives login results (both direct delivery and recovered results)
78+ * @param logoutCallback receives logout results recovered after a configuration change
8879 */
8980 @JvmStatic
90- public fun consumePendingLogoutResult (callback : Callback <Void ?, AuthenticationException >): Boolean {
91- val result = pendingLogoutResult.getAndSet(null ) ? : return false
92- when (result) {
93- is PendingResult .Success -> callback.onSuccess(result.result)
94- is PendingResult .Failure -> callback.onFailure(result.error)
95- }
96- resetManagerInstance()
97- return true
81+ public fun attach (
82+ lifecycleOwner : LifecycleOwner ,
83+ loginCallback : Callback <Credentials , AuthenticationException >,
84+ logoutCallback : Callback <Void ?, AuthenticationException >? = null,
85+ ) {
86+ // Process-death recovery: register and auto-remove on destroy
87+ callbacks + = loginCallback
88+ lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
89+ override fun onDestroy (owner : LifecycleOwner ) {
90+ callbacks - = loginCallback
91+ owner.lifecycle.removeObserver(this )
92+ }
93+ })
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+ }
98113 }
99114
115+ @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)" )
118+ )
100119 @JvmStatic
101120 public fun addCallback (callback : Callback <Credentials , AuthenticationException >) {
102121 callbacks + = callback
103122 }
104123
124+ @Deprecated(
125+ message = " Use attach() instead — it auto-removes the callback when the lifecycle owner is destroyed." ,
126+ replaceWith = ReplaceWith (" attach(lifecycleOwner, loginCallback = callback)" )
127+ )
105128 @JvmStatic
106129 public fun removeCallback (callback : Callback <Credentials , AuthenticationException >) {
107130 callbacks - = callback
@@ -198,7 +221,6 @@ public object WebAuthProvider {
198221 }
199222
200223 @JvmStatic
201- @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
202224 internal fun resetManagerInstance () {
203225 managerInstance = null
204226 }
@@ -304,7 +326,7 @@ public object WebAuthProvider {
304326
305327 val effectiveCallback = if (context is LifecycleOwner ) {
306328 LifecycleAwareCallback <Void ?>(
307- inner = callback,
329+ delegateCallback = callback,
308330 lifecycleOwner = context as LifecycleOwner ,
309331 onDetached = { _: Void ? , error: AuthenticationException ? ->
310332 if (error != null ) {
@@ -638,7 +660,6 @@ public object WebAuthProvider {
638660 return this
639661 }
640662
641- @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
642663 internal fun withPKCE (pkce : PKCE ): Builder {
643664 this .pkce = pkce
644665 return this
@@ -675,7 +696,7 @@ public object WebAuthProvider {
675696 pendingLoginResult.set(null )
676697 val effectiveCallback = if (context is LifecycleOwner ) {
677698 LifecycleAwareCallback <Credentials >(
678- inner = callback,
699+ delegateCallback = callback,
679700 lifecycleOwner = context as LifecycleOwner ,
680701 onDetached = { success: Credentials ? , error: AuthenticationException ? ->
681702 if (success != null ) {
0 commit comments