> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# handleRedirect

> Exchanges the grant on an inbound redirect URL for an AuthResult, or returns nil when the URL is not a Portal sign-in.

**Function Signature**

```swift theme={null}
public func handleRedirect(_ url: URL) async throws -> AuthResult?
public func handleRedirect(_ url: String) async throws -> AuthResult?
```

A `PortalAuth` method and the single completion path for every redirect-based flow: magic links and the `loginWithGoogle` / `loginWithApple` browser path. Forward every URL your app receives to it. `signInWithGoogle` / `signInWithApple` call it internally.

**Parameters**

* **`url`**: The URL iOS delivered to your app, as a `URL` (from `onOpenURL`, your scene delegate or your app delegate) or a `String`. The `URL` overload forwards `absoluteString`, so percent-encoding stays intact and the grant is decoded exactly once.

**Returns**

* **`AuthResult?`**:
  * **`nil`**: The URL is not for this instance: it does not match `redirectUrl`, carries no `token`, carries no recognized auth-method marker, is empty, or is longer than 8192 UTF-16 code units. Let your other URL handlers try.
  * **`.authenticated(AuthenticatedResult)`**: The sign-in is complete. `session` is already saved to the Keychain; pass it to `Portal(credentials:)`.
  * **`.totpRequired(TotpRequiredResult)`**: A two-factor code is required. Nothing is persisted; complete with [`verifyTotp`](./portalauthverifytotp).

**Throws**

* `PortalAuthError.authenticationFailed(error:)`: The URL matches `redirectUrl` and carries an `error` parameter, for example `oauth_failed`. Thrown even if a token is also present, and before any network call. The value is bounded to 100 characters.
* `PortalAuthError.invalidGrantResponse`: The exchange returned neither a session token nor a `userJwt`.
* `PortalAuthError.malformedResponse(path:missing:)`: The response did not carry the documented fields.
* `PortalAuthError.sessionStorageFailure(message:)`: The session could not be saved to the Keychain, so the sign-in is not returned. The grant is already spent; calling `handleRedirect` again with the same URL on the same instance retries the write instead of re-sending the grant.
* `PortalRequestsError.unauthorized`: Portal rejected the grant: it expired (15 minutes), was already exchanged, was issued for a different environment, or its sign-in method has since been disabled. Also thrown when a remembered session has since been invalidated. Offer the user a new sign-in.
* Other transport errors unchanged.

**Notes**

* **Safe to call twice with the same URL.** The instance remembers the last grant the backend accepted and replays the same `AuthResult` (and the same `PortalSession` instance) when iOS re-delivers the redirect, instead of failing Portal's single-use check. A redirect that failed on a dropped connection is not remembered, so it can be retried; if the backend spent the grant before the connection dropped, the retry throws `PortalRequestsError.unauthorized` and the user needs a new sign-in. The memory lives on the instance and in this process: `clearPersistedSession()` drops it, and after the app is terminated the recovery is `restoreSession()`.
* A grant that stopped at `.totpRequired` replays as the same step. Once `verifyTotp` accepts the code, the same grant replays as the `.authenticated` result it resolved to.
* Matching is case-insensitive on scheme and host and exact on path, after dropping the query, the fragment and trailing slashes. Register fragment-free redirect URLs.
* Routing: `authMethod=EMAIL_MAGIC_LINK` goes to the magic-link exchange; `login_type=GOOGLE` or `login_type=APPLE` goes to the OAuth exchange.
* A redirect delivered through the OS can reach another app that registers the same custom scheme. Prefer `signInWith*` for OAuth, and a Universal Link for magic links; see [Register your redirect](../guide/client-auth#register-your-redirect).
* Never log the raw URL; it contains the grant.
* Available starting from SDK version 8.0.0.

**Example Usage**

```swift theme={null}
import PortalSwift

func completeSignIn(with url: URL) async {
    do {
        guard let result = try await auth.handleRedirect(url) else {
            // Not a Portal sign-in redirect.
            return
        }

        switch result {
        case .authenticated(let authenticated):
            let portal = try Portal(credentials: authenticated.session)
            portal.onSessionInvalidated { [weak self] in
                self?.showSignIn()
            }
            self.portal = portal
        case .totpRequired(let step):
            presentTotpPrompt(step)
        }
    } catch PortalAuthError.authenticationFailed(let error) {
        print("Sign-in failed: \(error)")
    } catch PortalRequestsError.unauthorized {
        print("The sign-in link is no longer valid. Send a new one.")
    } catch {
        print("Error completing sign-in: \(error)")
    }
}
```

**Related Documentation**

* [sendMagicLink reference](./portalauthsendmagiclink)
* [loginWithGoogle reference](./portalauthloginwithgoogle)
* [verifyTotp reference](./portalauthverifytotp)
* [Complete the sign-in](../guide/client-auth#complete-the-sign-in)
* [Register your redirect](../guide/client-auth#register-your-redirect)
