stack/sdks/implementations/swift
BilalG1 c0fefd3b7a
feat(backend): dual-accept hexclave-mobile-oauth-url:// alongside legacy scheme (#1501)
## What

1. **Backend dual-accept**: `isAcceptedNativeAppUrl()` accepts both
`stack-auth-mobile-oauth-url://` (legacy) and
`hexclave-mobile-oauth-url://` (canonical).
2. **Swift SDK switches to the canonical scheme**: `StackAuth` Swift SDK
now emits and intercepts `hexclave-mobile-oauth-url://` for native-app
OAuth callbacks.

Before this PR, `hexclave-mobile-oauth-url` existed only inside
`RENAME-TO-HEXCLAVE.md` — not in any code.

## Why the Swift SDK change is safe

The Swift SDK uses
`ASWebAuthenticationSession(url:callbackURLScheme:completion:)`
([StackClientApp.swift:197-199](sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift#L197)).
With this API, iOS intercepts the callback scheme **ephemerally** — no
`Info.plist` registration is required. The Swift SDK source has no
`Info.plist`, and the example apps' `pbxproj` registers no
`CFBundleURLSchemes`. So:

- New customer builds against the updated SDK → emit new scheme →
backend accepts → `ASWebAuthenticationSession` intercepts on new scheme
→ works.
- Already-shipped customer App Store binaries on older SDK versions →
emit old scheme → backend still accepts → works.
- **No customer ever has to update an `Info.plist`.**

The only real backward-compat constraint is that the backend can never
drop the old scheme (already-shipped customer binaries have the constant
baked into them). Hence the dual-accept.

(Note: `RENAME-TO-HEXCLAVE.md` line 88 incorrectly attributes the
constraint to `Info.plist` registration. That's not how the SDK works —
the scheme is baked into the SDK binary, not the customer's plist. The
fix described in that doc is essentially the right shape; only the
mechanism description is wrong.)

## Changes

| File | Change |
|---|---|
| `packages/stack-shared/src/utils/redirect-urls.tsx` |
`isAcceptedNativeAppUrl()` accepts either protocol. |
| `apps/backend/src/lib/redirect-urls.test.tsx` | Adds positive
assertions for the new scheme in `isAcceptedNativeAppUrl`; parity
negative assertions in `validateRedirectUrl`. |
| `sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift` |
`callbackScheme` → `"hexclave-mobile-oauth-url"`; fatalError example
strings updated. |
| `sdks/implementations/swift/Tests/StackAuthTests/OAuthTests.swift` |
Test fixture URLs updated (no assertions depend on the scheme literal).
|
|
`sdks/implementations/swift/Examples/StackAuthiOS/.../StackAuthiOSApp.swift`
| Default values in the example UI. |
|
`sdks/implementations/swift/Examples/StackAuthMacOS/.../StackAuthMacOSApp.swift`
| Default values in the example UI. |
| `sdks/implementations/swift/README.md` | Documents the new canonical
scheme; compat note for the legacy one. |
| `sdks/spec/src/apps/client-app.spec.md` | New scheme is canonical;
legacy is "accepted indefinitely for already-shipped customer app
binaries built against older SDK versions." |

## Verification

- `pnpm test run apps/backend/src/lib/redirect-urls.test.tsx` — 34/34
passing (was 33; one new `it` block plus parity assertions).
- `pnpm --filter @stackframe/stack-shared --filter @stackframe/backend
run lint` — clean.
- `pnpm --filter @stackframe/stack-shared --filter @stackframe/backend
run typecheck` — clean.
- Swift assertions in `OAuthTests.swift` do not check the scheme literal
— they only check `oauth/authorize/<provider>`, state/verifier
non-emptiness, and that `redirectUrl` round-trips. The fixture-value
change is mechanical.

## Risk

Low. Backend behavior strictly widens (every URL accepted before is
still accepted). Swift SDK change is internal to OAuth callback
handling, requires no customer migration, and is paired with the backend
dual-accept landing in the same PR.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Adopted the canonical OAuth callback scheme
"hexclave-mobile-oauth-url://" for native apps while continuing to
accept the legacy "stack-auth-mobile-oauth-url://".

* **Documentation**
* Updated SDK docs, examples, and spec guidance to reference the
canonical callback scheme and clarify legacy acceptance.

* **Tests & Samples**
* Updated tests and example apps to use and validate the canonical
scheme.

* **Style**
* Rebranded the dev-tool trigger icon to the new Hexclave monochrome
logo.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1501?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-27 15:44:06 -07:00
..
Examples feat(backend): dual-accept hexclave-mobile-oauth-url:// alongside legacy scheme (#1501) 2026-05-27 15:44:06 -07:00
Sources/StackAuth feat(backend): dual-accept hexclave-mobile-oauth-url:// alongside legacy scheme (#1501) 2026-05-27 15:44:06 -07:00
Tests/StackAuthTests feat(backend): dual-accept hexclave-mobile-oauth-url:// alongside legacy scheme (#1501) 2026-05-27 15:44:06 -07:00
.gitignore [Fix] [Feat] Update OAuth Sign-In and Get Token Functions to Work (#1130) 2026-01-28 02:17:27 +00:00
package.json feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-05-26 19:18:20 -07:00
Package.resolved [Fix] [Feat] Update OAuth Sign-In and Get Token Functions to Work (#1130) 2026-01-28 02:17:27 +00:00
Package.swift [Fix] [Feat] Update OAuth Sign-In and Get Token Functions to Work (#1130) 2026-01-28 02:17:27 +00:00
README.md feat(backend): dual-accept hexclave-mobile-oauth-url:// alongside legacy scheme (#1501) 2026-05-27 15:44:06 -07:00

Hexclave Swift SDK

Swift SDK for Hexclave. Supports iOS, macOS, watchOS, tvOS, and visionOS.

Requirements

  • Swift 5.9+
  • iOS 15+ / macOS 12+ / watchOS 8+ / tvOS 15+ / visionOS 1+

Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/hexclave/swift-sdk-prerelease", from: <version>)
]

Quick Start

import StackAuth

let stack = StackClientApp(
    projectId: "your-project-id",
    publishableClientKey: "your-key"
)

// Sign in with email/password
try await stack.signInWithCredential(email: "user@example.com", password: "password")

// Get current user
if let user = try await stack.getUser() {
    print("Signed in as \(user.displayName ?? "Unknown")")
}

// Sign out
try await stack.signOut()

Design Decisions

Error Handling

All functions that can fail use Swift's native throws. Errors conform to StackAuthError:

do {
    try await stack.signInWithCredential(email: email, password: password)
} catch let error as StackAuthError {
    switch error.code {
    case "email_password_mismatch":
        print("Wrong password")
    default:
        print(error.message)
    }
}

Token Storage

  • Default: Keychain (secure, persists across app launches)
  • Option: Memory (for testing or ephemeral sessions)
  • Option: Custom TokenStoreProtocol implementation
// Memory storage (for testing)
let stack = StackClientApp(
    projectId: "...",
    publishableClientKey: "...",
    tokenStore: .memory
)

// Custom storage
let stack = StackClientApp(
    projectId: "...",
    publishableClientKey: "...",
    tokenStore: .custom(MyTokenStore())
)

OAuth Flows

Two approaches for OAuth authentication:

1. Integrated (recommended) - Uses ASWebAuthenticationSession:

// Opens auth session, handles callback automatically
// Uses fixed callback scheme: hexclave-mobile-oauth-url://
try await stack.signInWithOAuth(provider: "google")

2. Manual URL handling - For custom implementations:

Note: The hexclave-mobile-oauth-url:// scheme is automatically accepted (the legacy stack-auth-mobile-oauth-url:// scheme also remains accepted for backwards compatibility).

// Get the OAuth URL (must provide absolute URLs)
let oauth = try await stack.getOAuthUrl(
    provider: "google",
    redirectUrl: "hexclave-mobile-oauth-url://success",
    errorRedirectUrl: "hexclave-mobile-oauth-url://error"
)

// Open oauth.url in your own browser/webview
// Store oauth.state, oauth.codeVerifier, and oauth.redirectUrl

// When callback received:
try await stack.callOAuthCallback(
    url: callbackUrl,
    codeVerifier: oauth.codeVerifier,
    redirectUrl: oauth.redirectUrl
)

Async/Await

All async operations use Swift's native concurrency:

Task {
    let user = try await stack.getUser()
    let teams = try await user?.listTeams()
}

Key Differences from JavaScript SDK

Aspect JavaScript Swift
Token Storage Cookies Keychain
OAuth Browser redirect ASWebAuthenticationSession
Redirect methods Available Not available (browser-only)
React hooks useUser() etc. Not applicable

Not Available in Swift

The following are browser-only and not exposed:

  • redirectToSignIn(), redirectToSignUp(), etc.
  • Cookie-based token storage
  • redirectMethod constructor option

Examples

Interactive example apps are available for testing all SDK functions:

macOS Example

cd Examples/StackAuthMacOS
swift run

Features a sidebar-based UI for testing authentication, user management, teams, OAuth, tokens, and server-side operations.

iOS Example

cd Examples/StackAuthiOS
open Package.swift  # Opens in Xcode

Features a tab-based UI optimized for iOS with the same comprehensive SDK coverage.

Both examples include:

  • Configurable API endpoints
  • Real-time operation logs
  • Error testing scenarios (wrong password, unauthorized access, etc.)
  • Client and server app operations

Testing

Tests use Swift Testing framework against a running backend.

Running Tests

  1. Start the development server:

    pnpm dev
    
  2. Run tests:

    cd sdks/implementations/swift
    swift test
    

The tests connect to http://localhost:8102 (or ${NEXT_PUBLIC_STACK_PORT_PREFIX}02).

API Reference

See the SDK Specification for complete API documentation.