From d3150408d2fb261174dbf58f313e2230b1ee16b3 Mon Sep 17 00:00:00 2001
From: Matt Creaser <mattwcc@amazon.com>
Date: Tue, 1 Apr 2025 10:43:20 -0300
Subject: [PATCH 1/2] Move SignOut to usecase

---
 .../auth/cognito/AWSCognitoAuthPlugin.kt      |   6 +-
 .../auth/cognito/KotlinAuthFacadeInternal.kt  |  17 --
 .../auth/cognito/RealAWSCognitoAuthPlugin.kt  | 157 --------------
 .../cognito/usecases/AuthUseCaseFactory.kt    |   9 +
 .../ClearFederationToIdentityPoolUseCase.kt   |  69 ++++++
 .../auth/cognito/usecases/SignOutUseCase.kt   | 127 +++++++++++
 .../auth/cognito/AWSCognitoAuthPluginTest.kt  |   9 +-
 .../auth/cognito/AuthValidationTest.kt        |  13 +-
 ...learFederationToIdentityPoolUseCaseTest.kt | 110 ++++++++++
 .../cognito/usecases/SignOutUseCaseTest.kt    | 201 ++++++++++++++++++
 10 files changed, 532 insertions(+), 186 deletions(-)
 create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt
 create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt
 create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt
 create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt

diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt
index 38bbf0cdf..3be83e037 100644
--- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt
@@ -404,12 +404,12 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {
     override fun signOut(onComplete: Consumer<AuthSignOutResult>) = enqueue(
         onComplete,
         onError = ::throwIt
-    ) { queueFacade.signOut() }
+    ) { useCaseFactory.signOut().execute() }
 
     override fun signOut(options: AuthSignOutOptions, onComplete: Consumer<AuthSignOutResult>) = enqueue(
         onComplete,
         onError = ::throwIt
-    ) { queueFacade.signOut(options) }
+    ) { useCaseFactory.signOut().execute(options) }
 
     override fun deleteUser(onSuccess: Action, onError: Consumer<AuthException>) = enqueue(onSuccess, onError) {
         useCaseFactory.deleteUser().execute()
@@ -523,7 +523,7 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {
      * @param onError Error callback
      */
     fun clearFederationToIdentityPool(onSuccess: Action, onError: Consumer<AuthException>) =
-        enqueue(onSuccess, onError) { queueFacade.clearFederationToIdentityPool() }
+        enqueue(onSuccess, onError) { useCaseFactory.clearFederationToIdentityPool().execute() }
 
     fun fetchMFAPreference(onSuccess: Consumer<UserMFAPreference>, onError: Consumer<AuthException>) =
         enqueue(onSuccess, onError) { useCaseFactory.fetchMfaPreference().execute() }
diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt
index a2bc97903..ab1a7ea1d 100644
--- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt
@@ -22,10 +22,8 @@ import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions
 import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult
 import com.amplifyframework.auth.options.AuthConfirmSignInOptions
 import com.amplifyframework.auth.options.AuthSignInOptions
-import com.amplifyframework.auth.options.AuthSignOutOptions
 import com.amplifyframework.auth.options.AuthWebUISignInOptions
 import com.amplifyframework.auth.result.AuthSignInResult
-import com.amplifyframework.auth.result.AuthSignOutResult
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
 import kotlin.coroutines.suspendCoroutine
@@ -116,14 +114,6 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth
         delegate.handleWebUISignInResponse(intent)
     }
 
-    suspend fun signOut(): AuthSignOutResult = suspendCoroutine { continuation ->
-        delegate.signOut { continuation.resume(it) }
-    }
-
-    suspend fun signOut(options: AuthSignOutOptions): AuthSignOutResult = suspendCoroutine { continuation ->
-        delegate.signOut(options) { continuation.resume(it) }
-    }
-
     suspend fun federateToIdentityPool(
         providerToken: String,
         authProvider: AuthProvider,
@@ -137,11 +127,4 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth
             { continuation.resumeWithException(it) }
         )
     }
-
-    suspend fun clearFederationToIdentityPool() = suspendCoroutine { continuation ->
-        delegate.clearFederationToIdentityPool(
-            { continuation.resume(Unit) },
-            { continuation.resumeWithException(it) }
-        )
-    }
 }
diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt
index 5d9d9d7ef..329fcebc4 100644
--- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt
@@ -46,26 +46,18 @@ import com.amplifyframework.auth.cognito.helpers.isMfaSetupSelectionChallenge
 import com.amplifyframework.auth.cognito.helpers.value
 import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions
 import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions
-import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions
 import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions
 import com.amplifyframework.auth.cognito.options.AuthFlowType
 import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions
-import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult
 import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult
-import com.amplifyframework.auth.cognito.result.GlobalSignOutError
-import com.amplifyframework.auth.cognito.result.HostedUIError
-import com.amplifyframework.auth.cognito.result.RevokeTokenError
 import com.amplifyframework.auth.exceptions.InvalidStateException
 import com.amplifyframework.auth.exceptions.UnknownException
 import com.amplifyframework.auth.options.AuthConfirmSignInOptions
 import com.amplifyframework.auth.options.AuthSignInOptions
-import com.amplifyframework.auth.options.AuthSignOutOptions
 import com.amplifyframework.auth.options.AuthWebUISignInOptions
 import com.amplifyframework.auth.result.AuthSignInResult
-import com.amplifyframework.auth.result.AuthSignOutResult
 import com.amplifyframework.auth.result.step.AuthNextSignInStep
 import com.amplifyframework.auth.result.step.AuthSignInStep
-import com.amplifyframework.core.Action
 import com.amplifyframework.core.Amplify
 import com.amplifyframework.core.Consumer
 import com.amplifyframework.hub.HubChannel
@@ -78,7 +70,6 @@ import com.amplifyframework.statemachine.codegen.data.FederatedToken
 import com.amplifyframework.statemachine.codegen.data.HostedUIErrorData
 import com.amplifyframework.statemachine.codegen.data.SignInData
 import com.amplifyframework.statemachine.codegen.data.SignInMethod
-import com.amplifyframework.statemachine.codegen.data.SignOutData
 import com.amplifyframework.statemachine.codegen.data.WebAuthnSignInContext
 import com.amplifyframework.statemachine.codegen.data.challengeNameType
 import com.amplifyframework.statemachine.codegen.errors.SessionError
@@ -980,114 +971,6 @@ internal class RealAWSCognitoAuthPlugin(
         }
     }
 
-    fun signOut(onComplete: Consumer<AuthSignOutResult>) {
-        signOut(AuthSignOutOptions.builder().build(), onComplete)
-    }
-
-    fun signOut(options: AuthSignOutOptions, onComplete: Consumer<AuthSignOutResult>) {
-        authStateMachine.getCurrentState { authState ->
-            when (authState.authNState) {
-                is AuthenticationState.NotConfigured ->
-                    onComplete.accept(AWSCognitoAuthSignOutResult.CompleteSignOut)
-                // Continue sign out and clear auth or guest credentials
-                is AuthenticationState.SignedIn, is AuthenticationState.SignedOut -> {
-                    // Send SignOut event here instead of OnSubscribedCallback handler to ensure we do not fire
-                    // onComplete immediately, which would happen if calling signOut while signed out
-                    val event = AuthenticationEvent(
-                        AuthenticationEvent.EventType.SignOutRequested(
-                            SignOutData(
-                                options.isGlobalSignOut,
-                                (options as? AWSCognitoAuthSignOutOptions)?.browserPackage
-                            )
-                        )
-                    )
-                    authStateMachine.send(event)
-                    _signOut(onComplete = onComplete)
-                }
-                is AuthenticationState.FederatedToIdentityPool -> {
-                    onComplete.accept(
-                        AWSCognitoAuthSignOutResult.FailedSignOut(
-                            InvalidStateException(
-                                "The user is currently federated to identity pool. " +
-                                    "You must call clearFederationToIdentityPool to clear credentials."
-                            )
-                        )
-                    )
-                }
-                else -> onComplete.accept(
-                    AWSCognitoAuthSignOutResult.FailedSignOut(InvalidStateException())
-                )
-            }
-        }
-    }
-
-    private fun _signOut(sendHubEvent: Boolean = true, onComplete: Consumer<AuthSignOutResult>) {
-        val token = StateChangeListenerToken()
-        var cancellationException: UserCancelledException? = null
-        authStateMachine.listen(
-            token,
-            { authState ->
-                if (authState is AuthState.Configured) {
-                    val (authNState, authZState) = authState
-                    when {
-                        authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> {
-                            authStateMachine.cancel(token)
-                            if (authNState.signedOutData.hasError) {
-                                val signedOutData = authNState.signedOutData
-                                onComplete.accept(
-                                    AWSCognitoAuthSignOutResult.PartialSignOut(
-                                        hostedUIError = signedOutData.hostedUIErrorData?.let { HostedUIError(it) },
-                                        globalSignOutError = signedOutData.globalSignOutErrorData?.let {
-                                            GlobalSignOutError(it)
-                                        },
-                                        revokeTokenError = signedOutData.revokeTokenErrorData?.let {
-                                            RevokeTokenError(
-                                                it
-                                            )
-                                        }
-                                    )
-                                )
-                                if (sendHubEvent) {
-                                    sendHubEvent(AuthChannelEventName.SIGNED_OUT.toString())
-                                }
-                            } else {
-                                onComplete.accept(AWSCognitoAuthSignOutResult.CompleteSignOut)
-                                if (sendHubEvent) {
-                                    sendHubEvent(AuthChannelEventName.SIGNED_OUT.toString())
-                                }
-                            }
-                        }
-                        authNState is AuthenticationState.Error -> {
-                            authStateMachine.cancel(token)
-                            onComplete.accept(
-                                AWSCognitoAuthSignOutResult.FailedSignOut(
-                                    CognitoAuthExceptionConverter.lookup(authNState.exception, "Sign out failed.")
-                                )
-                            )
-                        }
-                        authNState is AuthenticationState.SigningOut -> {
-                            val state = authNState.signOutState
-                            if (state is SignOutState.Error && state.exception is UserCancelledException) {
-                                cancellationException = state.exception
-                            }
-                        }
-                        authNState is AuthenticationState.SignedIn && cancellationException != null -> {
-                            authStateMachine.cancel(token)
-                            cancellationException?.let {
-                                onComplete.accept(AWSCognitoAuthSignOutResult.FailedSignOut(it))
-                            }
-                        }
-                        else -> {
-                            // No - op
-                        }
-                    }
-                }
-            },
-            {
-            }
-        )
-    }
-
     private fun addAuthStateChangeListener() {
         authStateMachine.listen(
             StateChangeListenerToken(),
@@ -1219,46 +1102,6 @@ internal class RealAWSCognitoAuthPlugin(
         )
     }
 
-    fun clearFederationToIdentityPool(onSuccess: Action, onError: Consumer<AuthException>) {
-        authStateMachine.getCurrentState { authState ->
-            val authNState = authState.authNState
-            val authZState = authState.authZState
-            when {
-                authState is AuthState.Configured &&
-                    (
-                        authNState is AuthenticationState.FederatedToIdentityPool &&
-                            authZState is AuthorizationState.SessionEstablished
-                        ) ||
-                    (
-                        authZState is AuthorizationState.Error &&
-                            authZState.exception is SessionError &&
-                            authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated
-                        ) -> {
-                    val event = AuthenticationEvent(AuthenticationEvent.EventType.ClearFederationToIdentityPool())
-                    authStateMachine.send(event)
-                    _clearFederationToIdentityPool(onSuccess, onError)
-                }
-                else -> {
-                    onError.accept(InvalidStateException("Clearing of federation failed."))
-                }
-            }
-        }
-    }
-
-    private fun _clearFederationToIdentityPool(onSuccess: Action, onError: Consumer<AuthException>) {
-        _signOut(sendHubEvent = false) {
-            when (it) {
-                is AWSCognitoAuthSignOutResult.FailedSignOut -> {
-                    onError.accept(it.exception)
-                }
-                else -> {
-                    onSuccess.call()
-                    sendHubEvent(AWSCognitoAuthChannelEventName.FEDERATION_TO_IDENTITY_POOL_CLEARED.toString())
-                }
-            }
-        }
-    }
-
     private fun sendHubEvent(eventName: String) {
         if (lastPublishedHubEventName.get() != eventName) {
             lastPublishedHubEventName.set(eventName)
diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt
index 584cd2f68..1a3b6640e 100644
--- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt
@@ -164,4 +164,13 @@ internal class AuthUseCaseFactory(
         fetchAuthSession = fetchAuthSession(),
         stateMachine = stateMachine
     )
+
+    fun signOut() = SignOutUseCase(
+        stateMachine = stateMachine
+    )
+
+    fun clearFederationToIdentityPool() = ClearFederationToIdentityPoolUseCase(
+        stateMachine = stateMachine,
+        signOut = signOut()
+    )
 }
diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt
new file mode 100644
index 000000000..a06360c3a
--- /dev/null
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.amplifyframework.auth.cognito.usecases
+
+import com.amplifyframework.auth.cognito.AWSCognitoAuthChannelEventName
+import com.amplifyframework.auth.cognito.AuthStateMachine
+import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult
+import com.amplifyframework.auth.exceptions.InvalidStateException
+import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter
+import com.amplifyframework.statemachine.codegen.data.AmplifyCredential
+import com.amplifyframework.statemachine.codegen.errors.SessionError
+import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent
+import com.amplifyframework.statemachine.codegen.states.AuthState
+import com.amplifyframework.statemachine.codegen.states.AuthenticationState
+import com.amplifyframework.statemachine.codegen.states.AuthorizationState
+
+internal class ClearFederationToIdentityPoolUseCase(
+    private val stateMachine: AuthStateMachine,
+    private val signOut: SignOutUseCase,
+    private val emitter: AuthHubEventEmitter = AuthHubEventEmitter()
+) {
+    suspend fun execute() {
+        val authState = stateMachine.getCurrentState()
+
+        when {
+            authState.isFederatedToIdentityPool() -> {
+                val event = AuthenticationEvent(AuthenticationEvent.EventType.ClearFederationToIdentityPool())
+                stateMachine.send(event)
+
+                when (val result = signOut.completeSignOut(sendHubEvent = false)) {
+                    is AWSCognitoAuthSignOutResult.FailedSignOut -> throw result.exception
+                    else -> emitter.sendHubEvent(
+                        AWSCognitoAuthChannelEventName.FEDERATION_TO_IDENTITY_POOL_CLEARED.toString()
+                    )
+                }
+            }
+            else -> throw InvalidStateException("Clearing of federation failed.")
+        }
+    }
+
+    private fun AuthState.isFederatedToIdentityPool(): Boolean {
+        val authNState = this.authNState
+        val authZState = this.authZState
+
+        return this is AuthState.Configured &&
+            (
+                authNState is AuthenticationState.FederatedToIdentityPool &&
+                    authZState is AuthorizationState.SessionEstablished
+                ) ||
+            (
+                authZState is AuthorizationState.Error &&
+                    authZState.exception is SessionError &&
+                    authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated
+                )
+    }
+}
diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt
new file mode 100644
index 000000000..d70501cde
--- /dev/null
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.amplifyframework.auth.cognito.usecases
+
+import com.amplifyframework.auth.AuthChannelEventName
+import com.amplifyframework.auth.cognito.AuthStateMachine
+import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter
+import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException
+import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions
+import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult
+import com.amplifyframework.auth.cognito.result.GlobalSignOutError
+import com.amplifyframework.auth.cognito.result.HostedUIError
+import com.amplifyframework.auth.cognito.result.RevokeTokenError
+import com.amplifyframework.auth.exceptions.InvalidStateException
+import com.amplifyframework.auth.options.AuthSignOutOptions
+import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter
+import com.amplifyframework.auth.result.AuthSignOutResult
+import com.amplifyframework.statemachine.codegen.data.SignOutData
+import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent
+import com.amplifyframework.statemachine.codegen.states.AuthState
+import com.amplifyframework.statemachine.codegen.states.AuthenticationState
+import com.amplifyframework.statemachine.codegen.states.AuthorizationState
+import com.amplifyframework.statemachine.codegen.states.SignOutState
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.mapNotNull
+
+internal class SignOutUseCase(
+    private val stateMachine: AuthStateMachine,
+    private val emitter: AuthHubEventEmitter = AuthHubEventEmitter()
+) {
+
+    suspend fun execute(options: AuthSignOutOptions = AuthSignOutOptions.builder().build()): AuthSignOutResult {
+        val authState = stateMachine.getCurrentState()
+        return when (authState.authNState) {
+            is AuthenticationState.NotConfigured -> AWSCognitoAuthSignOutResult.CompleteSignOut
+            // Continue sign out and clear auth or guest credentials
+            is AuthenticationState.SignedIn, is AuthenticationState.SignedOut -> {
+                // Send SignOut event here instead of OnSubscribedCallback handler to ensure we do not fire
+                // onComplete immediately, which would happen if calling signOut while signed out
+                sendSignOutRequest(options)
+                completeSignOut(sendHubEvent = true)
+            }
+            is AuthenticationState.FederatedToIdentityPool -> {
+                AWSCognitoAuthSignOutResult.FailedSignOut(
+                    InvalidStateException(
+                        "The user is currently federated to identity pool. " +
+                            "You must call clearFederationToIdentityPool to clear credentials."
+                    )
+                )
+            }
+            else -> AWSCognitoAuthSignOutResult.FailedSignOut(InvalidStateException())
+        }
+    }
+
+    suspend fun completeSignOut(sendHubEvent: Boolean): AuthSignOutResult {
+        var cancellationException: UserCancelledException? = null
+
+        val result = stateMachine.stateTransitions.mapNotNull { authState ->
+            if (authState !is AuthState.Configured) {
+                return@mapNotNull null
+            }
+
+            val (authNState, authZState) = authState
+
+            when {
+                authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> {
+                    if (sendHubEvent) {
+                        emitter.sendHubEvent(AuthChannelEventName.SIGNED_OUT.toString())
+                    }
+                    if (authNState.signedOutData.hasError) {
+                        val signedOutData = authNState.signedOutData
+                        AWSCognitoAuthSignOutResult.PartialSignOut(
+                            hostedUIError = signedOutData.hostedUIErrorData?.let { HostedUIError(it) },
+                            globalSignOutError = signedOutData.globalSignOutErrorData?.let { GlobalSignOutError(it) },
+                            revokeTokenError = signedOutData.revokeTokenErrorData?.let { RevokeTokenError(it) }
+                        )
+                    } else {
+                        AWSCognitoAuthSignOutResult.CompleteSignOut
+                    }
+                }
+                authNState is AuthenticationState.Error -> {
+                    AWSCognitoAuthSignOutResult.FailedSignOut(
+                        CognitoAuthExceptionConverter.lookup(authNState.exception, "Sign out failed.")
+                    )
+                }
+                authNState is AuthenticationState.SigningOut -> {
+                    val state = authNState.signOutState
+                    if (state is SignOutState.Error && state.exception is UserCancelledException) {
+                        cancellationException = state.exception
+                    }
+                    null
+                }
+                authNState is AuthenticationState.SignedIn && cancellationException != null -> {
+                    AWSCognitoAuthSignOutResult.FailedSignOut(cancellationException!!)
+                }
+                else -> null // no-op
+            }
+        }.first()
+
+        return result
+    }
+
+    private fun sendSignOutRequest(options: AuthSignOutOptions) {
+        val event = AuthenticationEvent(
+            AuthenticationEvent.EventType.SignOutRequested(
+                SignOutData(
+                    options.isGlobalSignOut,
+                    (options as? AWSCognitoAuthSignOutOptions)?.browserPackage
+                )
+            )
+        )
+        stateMachine.send(event)
+    }
+}
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt
index b6f872008..02ed29f21 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt
@@ -624,9 +624,10 @@ class AWSCognitoAuthPluginTest {
     fun verifySignOut() {
         val expectedOnComplete = Consumer<AuthSignOutResult> { }
 
+        val useCase = authPlugin.useCaseFactory.signOut()
         authPlugin.signOut(expectedOnComplete)
 
-        verify(timeout = CHANNEL_TIMEOUT) { realPlugin.signOut(any()) }
+        coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() }
     }
 
     @Test
@@ -634,9 +635,10 @@ class AWSCognitoAuthPluginTest {
         val expectedOptions = AuthSignOutOptions.builder().build()
         val expectedOnComplete = Consumer<AuthSignOutResult> { }
 
+        val useCase = authPlugin.useCaseFactory.signOut()
         authPlugin.signOut(expectedOptions, expectedOnComplete)
 
-        verify(timeout = CHANNEL_TIMEOUT) { realPlugin.signOut(expectedOptions, any()) }
+        coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedOptions) }
     }
 
     @Test
@@ -700,9 +702,10 @@ class AWSCognitoAuthPluginTest {
         val expectedOnSuccess = Action { }
         val expectedOnError = Consumer<AuthException> { }
 
+        val useCase = authPlugin.useCaseFactory.clearFederationToIdentityPool()
         authPlugin.clearFederationToIdentityPool(expectedOnSuccess, expectedOnError)
 
-        verify(timeout = CHANNEL_TIMEOUT) { realPlugin.clearFederationToIdentityPool(any(), any()) }
+        coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() }
     }
 
     @Test
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt
index 0ec803337..2f0020811 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt
@@ -26,6 +26,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserNotFoundExcepti
 import com.amplifyframework.auth.AuthException
 import com.amplifyframework.auth.cognito.featuretest.generators.authstategenerators.AuthStateJsonGenerator.DUMMY_TOKEN
 import com.amplifyframework.auth.cognito.helpers.AuthHelper
+import com.amplifyframework.auth.cognito.usecases.SignOutUseCase
 import com.amplifyframework.auth.result.AuthSignInResult
 import com.amplifyframework.core.Consumer
 import com.amplifyframework.logging.Logger
@@ -134,6 +135,10 @@ class AuthValidationTest {
         logger = logger
     )
 
+    private val signOutUseCase = SignOutUseCase(
+        stateMachine = stateMachine
+    )
+
     private val mainThreadSurrogate = newSingleThreadContext("Main thread")
 
 //region Setup/Teardown
@@ -453,9 +458,7 @@ class AuthValidationTest {
         }
     }
 
-    private fun signOut() = blockForResult { complete ->
-        plugin.signOut(complete)
-    }
+    private fun signOut() = runBlocking { withTimeout(10000L) { signOutUseCase.execute() } }
 
     private fun signInHostedUi(): AuthSignInResult {
         every { hostedUIClient.launchCustomTabsSignIn(any()) } answers {
@@ -470,9 +473,7 @@ class AuthValidationTest {
         }
     }
 
-    private fun signOutHostedUi() = blockForResult { complete ->
-        plugin.signOut(complete)
-    }
+    private fun signOutHostedUi() = signOut()
 
     private fun assertSignedOut() {
         val result = blockForResult { continuation -> stateMachine.getCurrentState { continuation.accept(it) } }
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt
new file mode 100644
index 000000000..a81cbe6bd
--- /dev/null
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.amplifyframework.auth.cognito.usecases
+
+import com.amplifyframework.auth.AuthException
+import com.amplifyframework.auth.cognito.AuthStateMachine
+import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult
+import com.amplifyframework.auth.cognito.testUtil.authState
+import com.amplifyframework.auth.cognito.testUtil.withAuthEvent
+import com.amplifyframework.auth.exceptions.InvalidStateException
+import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter
+import com.amplifyframework.statemachine.codegen.data.AmplifyCredential
+import com.amplifyframework.statemachine.codegen.errors.SessionError
+import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent
+import com.amplifyframework.statemachine.codegen.states.AuthState
+import com.amplifyframework.statemachine.codegen.states.AuthenticationState
+import com.amplifyframework.statemachine.codegen.states.AuthorizationState
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.assertions.throwables.shouldThrowAny
+import io.kotest.matchers.shouldBe
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.justRun
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ClearFederationToIdentityPoolUseCaseTest {
+    private val credential = AmplifyCredential.IdentityPoolFederated(mockk(), "id", mockk())
+    private val stateFlow = MutableStateFlow<AuthState>(
+        authState(
+            authNState = AuthenticationState.FederatedToIdentityPool(),
+            authZState = AuthorizationState.SessionEstablished(credential)
+        )
+    )
+
+    private val stateMachine: AuthStateMachine = mockk {
+        every { state } returns stateFlow
+        every { stateTransitions } answers { stateFlow.drop(1) }
+        coEvery { getCurrentState() } answers { stateFlow.value }
+        justRun { send(any()) }
+    }
+
+    private val signOut: SignOutUseCase = mockk {
+        coEvery { completeSignOut(any()) } returns AWSCognitoAuthSignOutResult.CompleteSignOut
+    }
+    private val emitter: AuthHubEventEmitter = mockk(relaxed = true)
+
+    private val useCase = ClearFederationToIdentityPoolUseCase(
+        stateMachine = stateMachine,
+        signOut = signOut,
+        emitter = emitter
+    )
+
+    @Test
+    fun `throws InvalidStateException if not federated sign in`() = runTest {
+        stateFlow.value = authState(authNState = AuthenticationState.SignedIn(mockk(), mockk()))
+
+        shouldThrow<InvalidStateException> {
+            useCase.execute()
+        }
+    }
+
+    @Test
+    fun `sends event if federated sign in`() = runTest {
+        useCase.execute()
+
+        coVerify {
+            stateMachine.send(withAuthEvent<AuthenticationEvent.EventType.ClearFederationToIdentityPool>())
+        }
+    }
+
+    @Test
+    fun `sends event if error state for federated sign in`() = runTest {
+        val exception = Exception()
+        stateFlow.value = authState(
+            authZState = AuthorizationState.Error(exception = SessionError(exception, credential))
+        )
+
+        useCase.execute()
+
+        coVerify {
+            stateMachine.send(withAuthEvent<AuthenticationEvent.EventType.ClearFederationToIdentityPool>())
+        }
+    }
+
+    @Test
+    fun `throws exception from failed sign out`() = runTest {
+        val exception = AuthException("failed", "test")
+        coEvery { signOut.completeSignOut(any()) } returns AWSCognitoAuthSignOutResult.FailedSignOut(exception)
+
+        shouldThrowAny { useCase.execute() } shouldBe exception
+    }
+}
diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt
new file mode 100644
index 000000000..e3b207f9d
--- /dev/null
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.amplifyframework.auth.cognito.usecases
+
+import com.amplifyframework.auth.cognito.AuthStateMachine
+import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException
+import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions
+import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult
+import com.amplifyframework.auth.cognito.testUtil.authState
+import com.amplifyframework.auth.cognito.testUtil.withAuthEvent
+import com.amplifyframework.auth.exceptions.InvalidStateException
+import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter
+import com.amplifyframework.statemachine.codegen.data.HostedUIErrorData
+import com.amplifyframework.statemachine.codegen.data.SignedOutData
+import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent
+import com.amplifyframework.statemachine.codegen.states.AuthState
+import com.amplifyframework.statemachine.codegen.states.AuthenticationState
+import com.amplifyframework.statemachine.codegen.states.AuthorizationState
+import com.amplifyframework.statemachine.codegen.states.SignOutState
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.types.shouldBeInstanceOf
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.justRun
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class SignOutUseCaseTest {
+    private val stateFlow = MutableStateFlow<AuthState>(
+        authState(
+            authNState = AuthenticationState.SignedIn(mockk(), mockk())
+        )
+    )
+
+    private val stateMachine: AuthStateMachine = mockk {
+        every { state } returns stateFlow
+        every { stateTransitions } answers { stateFlow.drop(1) }
+        coEvery { getCurrentState() } answers { stateFlow.value }
+        justRun { send(any()) }
+    }
+
+    private val emitter: AuthHubEventEmitter = mockk(relaxed = true)
+
+    private val useCase = SignOutUseCase(
+        stateMachine = stateMachine,
+        emitter = emitter
+    )
+
+    @Test
+    fun `sends sign out event`() = runTest {
+        backgroundScope.launch { useCase.execute() }
+        runCurrent()
+
+        verify {
+            stateMachine.send(
+                withAuthEvent<AuthenticationEvent.EventType.SignOutRequested> { event ->
+                    event.signOutData.globalSignOut shouldBe false
+                    event.signOutData.browserPackage shouldBe null
+                }
+            )
+        }
+    }
+
+    @Test
+    fun `uses supplied options in sign out event`() = runTest {
+        val options = AWSCognitoAuthSignOutOptions.builder()
+            .globalSignOut(true)
+            .browserPackage("foo")
+            .build()
+
+        backgroundScope.launch { useCase.execute(options) }
+        runCurrent()
+
+        verify {
+            stateMachine.send(
+                withAuthEvent<AuthenticationEvent.EventType.SignOutRequested> { event ->
+                    event.signOutData.globalSignOut shouldBe true
+                    event.signOutData.browserPackage shouldBe "foo"
+                }
+            )
+        }
+    }
+
+    @Test
+    fun `succeeds if not configured`() = runTest {
+        stateFlow.value = authState(authNState = AuthenticationState.NotConfigured())
+
+        val result = useCase.execute()
+
+        result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.CompleteSignOut>()
+    }
+
+    @Test
+    fun `fails if sign in is federated`() = runTest {
+        stateFlow.value = authState(authNState = AuthenticationState.FederatedToIdentityPool())
+
+        val result = useCase.execute()
+
+        val failed = result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.FailedSignOut>()
+        failed.exception.shouldBeInstanceOf<InvalidStateException>()
+    }
+
+    @Test
+    fun `fails if in unexpected state`() = runTest {
+        stateFlow.value = authState(authNState = AuthenticationState.SigningIn())
+
+        val result = useCase.execute()
+
+        val failed = result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.FailedSignOut>()
+        failed.exception.shouldBeInstanceOf<InvalidStateException>()
+    }
+
+    @Test
+    fun `fails if user cancels sign out`() = runTest {
+        val deferred = backgroundScope.async { useCase.execute() }
+        runCurrent()
+
+        val exception = UserCancelledException("failed", "test")
+        stateFlow.value = authState(authNState = AuthenticationState.SigningOut(SignOutState.Error(exception)))
+        runCurrent()
+
+        stateFlow.value = authState(authNState = AuthenticationState.SignedIn(mockk(), mockk()))
+
+        val result = deferred.await()
+        val failed = result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.FailedSignOut>()
+        failed.exception shouldBe exception
+    }
+
+    @Test
+    fun `fails if reaching error state`() = runTest {
+        val deferred = backgroundScope.async { useCase.execute() }
+        runCurrent()
+
+        val exception = Exception()
+        stateFlow.value = authState(authNState = AuthenticationState.Error(exception = exception))
+
+        val result = deferred.await()
+        val failed = result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.FailedSignOut>()
+        failed.exception.cause shouldBe exception
+    }
+
+    @Test
+    fun `returns complete result`() = runTest {
+        val deferred = backgroundScope.async { useCase.execute() }
+        runCurrent()
+
+        val signedOutData = SignedOutData()
+
+        stateFlow.value = authState(
+            authNState = AuthenticationState.SignedOut(signedOutData),
+            authZState = AuthorizationState.Configured()
+        )
+
+        val result = deferred.await()
+        result shouldBe AWSCognitoAuthSignOutResult.CompleteSignOut
+    }
+
+    @Test
+    fun `returns partial result`() = runTest {
+        val deferred = backgroundScope.async { useCase.execute() }
+        runCurrent()
+
+        val exception = Exception()
+        val signedOutData = SignedOutData(
+            hostedUIErrorData = HostedUIErrorData("url", exception)
+        )
+
+        stateFlow.value = authState(
+            authNState = AuthenticationState.SignedOut(signedOutData),
+            authZState = AuthorizationState.Configured()
+        )
+
+        val result = deferred.await()
+        val partial = result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.PartialSignOut>()
+
+        partial.hostedUIError?.url shouldBe "url"
+        partial.hostedUIError?.exception shouldBe exception
+    }
+}

From f98699b4e12303fed4019fba15e92017648b00d3 Mon Sep 17 00:00:00 2001
From: Matt Creaser <mattwcc@amazon.com>
Date: Wed, 16 Apr 2025 15:45:10 -0300
Subject: [PATCH 2/2] Fix timeout in AuthValidationTest to match previous value

---
 .../com/amplifyframework/auth/cognito/AuthValidationTest.kt     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt
index 2f0020811..d5b11de3d 100644
--- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt
@@ -458,7 +458,7 @@ class AuthValidationTest {
         }
     }
 
-    private fun signOut() = runBlocking { withTimeout(10000L) { signOutUseCase.execute() } }
+    private fun signOut() = runBlocking { withTimeout(100000L) { signOutUseCase.execute() } }
 
     private fun signInHostedUi(): AuthSignInResult {
         every { hostedUIClient.launchCustomTabsSignIn(any()) } answers {