Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ While the SDK can be configured in code, it defaults to reading from `Auth0.plis

## Dependencies

- **JWTDecode.swift**: For decoding JWTs to extract claims/expiry.
- **JWTDecode.swift**: For decoding JWTs to extract claims/expiry (v4.0+, Swift 6 compliant).
- **SimpleKeychain**: For Keychain access (iOS/macOS).
- **Quick/Nimble**: (Test Target only) Behavior-driven testing.

Expand Down
5 changes: 5 additions & 0 deletions App/Auth0DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import SwiftUI
struct Auth0DemoApp: App {
var body: some Scene {
WindowGroup {
#if os(macOS)
ContentView()
#else
ContentView()
.withWindowReader()
#endif
}
}
}
173 changes: 135 additions & 38 deletions App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,87 @@ import SwiftUI
import Combine
import Auth0

#if !os(macOS)
import UIKit
#else
import AppKit
#endif


struct ContentView: View {
@StateObject private var viewModel = ContentViewModel()

@StateObject private var viewModel = ContentViewModel(authenticationClient: Auth0.authentication())

#if os(macOS)
@State private var currentWindow: Auth0WindowRepresentable?
#else
@Environment(\.window) private var window
#endif

var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {

// Web Login Button (Universal Login)
Button {
Task {
await viewModel.webLogin()
}
} label: {
Text("Login with Browser")
}
.buttonStyle(SecondaryButtonStyle())
.disabled(viewModel.isLoading)

// Logout Button
Button {
Task {
await viewModel.logout()
}
} label: {
Text("Logout")
}
.buttonStyle(PrimaryButtonStyle())
.disabled(viewModel.isLoading || !viewModel.isAuthenticated)

// Authentication Status
if viewModel.isAuthenticated {
Text("✓ Authenticated")
.foregroundColor(.green)
.font(.caption)
}
}
.padding(.horizontal)
.padding(.top, 10)
VStack(spacing: 20) {

Button {
Task {

#if WEB_AUTH_PLATFORM
#if os(macOS)
await viewModel.webLogin(presentationWindow: currentWindow)
#else
await viewModel.webLogin(presentationWindow: window)
#endif
#endif
}
} label: {
VStack(spacing: 4) {
Text("Login")
}
}
.buttonStyle(PrimaryButtonStyle())
.disabled(viewModel.isLoading)

Divider()
.padding(.vertical)

Button {
Task {
#if WEB_AUTH_PLATFORM
#if os(macOS)
await viewModel.logout(presentationWindow: currentWindow)
#else
await viewModel.logout(presentationWindow: window)
#endif
#endif
}
} label: {
Text("Logout")
}
.buttonStyle(PrimaryButtonStyle())
.disabled(viewModel.isLoading || !viewModel.isAuthenticated)

if viewModel.isAuthenticated {
Text("✓ Authenticated")
.foregroundColor(.green)
.font(.caption)
}

if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
}
}
.padding(.horizontal)
.padding(.top, 10)
.task {
// Check for existing credentials on appear
await viewModel.checkAuthentication()
}
#if os(macOS)
.onAppear {
// Capture the window on appear for macOS
currentWindow = getCurrentWindow()
}
#endif
}
}

Expand Down Expand Up @@ -78,3 +114,64 @@ struct SecondaryButtonStyle: ButtonStyle {
}
}

#if os(macOS)
private func getCurrentWindow() -> NSWindow? {
if let keyWindow = NSApplication.shared.keyWindow {
return keyWindow
}

if let mainWindow = NSApplication.shared.mainWindow {
return mainWindow
}

return NSApplication.shared.windows.first
}

#else
private struct WindowKey: EnvironmentKey {
static let defaultValue: UIWindow? = nil
}

extension EnvironmentValues {
var window: UIWindow? {
get { self[WindowKey.self] }
set { self[WindowKey.self] = newValue }
}
}

struct WindowReaderModifier: ViewModifier {
@State private var window: UIWindow?

func body(content: Content) -> some View {
content
.environment(\.window, window)
.background(
WindowAccessor(window: $window)
)
}
}

struct WindowAccessor: UIViewRepresentable {
@Binding var window: UIWindow?

func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
return view
}

func updateUIView(_ uiView: UIView, context: Context) {
DispatchQueue.main.async {
self.window = uiView.window
}
}
}

extension View {
func withWindowReader() -> some View {
self.modifier(WindowReaderModifier())
}
}

#endif

74 changes: 48 additions & 26 deletions App/ContentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,77 @@ import Combine

@MainActor
final class ContentViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var isAuthenticated: Bool = false
private let credentialsManager = CredentialsManager(authentication: Auth0.authentication())

/// Web Authentication using Universal Login (Recommended)
func webLogin() async {
private let credentialsManager: CredentialsManager

private let authenticationClient: Authentication
init(email: String = "",
password: String = "",
isLoading: Bool = false,
errorMessage: String? = nil,
isAuthenticated: Bool = false,
authenticationClient: Authentication,
credentialsManager: CredentialsManager? = nil) {
self.email = email
self.password = password
self.isLoading = isLoading
self.errorMessage = errorMessage
self.isAuthenticated = isAuthenticated
self.authenticationClient = authenticationClient
self.credentialsManager = credentialsManager ?? CredentialsManager(authentication: Auth0.authentication())
}

func login() async {
isLoading = true
do {
let credentials = try await authenticationClient
.login(usernameOrEmail: email, password: password, realmOrConnection: "Username-Password-Authentication", audience: nil, scope: "openid profile offline_access")
.start()
isAuthenticated = true
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}

#if WEB_AUTH_PLATFORM
func webLogin(presentationWindow window: Auth0WindowRepresentable? = nil) async {
isLoading = true
errorMessage = nil

#if !os(tvOS) && !os(watchOS)

do {
let credentials = try await Auth0
.webAuth()
.scope("openid profile email offline_access")
.start()

// Store credentials securely

let stored = credentialsManager.store(credentials: credentials)
if stored {
isAuthenticated = true
print("Access Token: \(credentials.accessToken)")
} else {
errorMessage = "Failed to store credentials"
}
} catch let error as Auth0Error {
errorMessage = "Login failed: \(error.localizedDescription)"
print("Web login failed with error: \(error)")
} catch {
errorMessage = "Unexpected error: \(error.localizedDescription)"
}
#endif


isLoading = false
}

/// Logout and clear stored credentials
func logout() async {
#endif

#if WEB_AUTH_PLATFORM
func logout(presentationWindow window: Auth0WindowRepresentable? = nil) async {
isLoading = true
errorMessage = nil
#if !os(tvOS) && !os(watchOS)
do {
try await Auth0
.webAuth()
.clearSession()

// Clear stored credentials
try await Auth0.webAuth().logout()

let cleared = credentialsManager.clear()
if cleared {
isAuthenticated = false
Expand All @@ -62,12 +86,11 @@ final class ContentViewModel: ObservableObject {
} catch {
errorMessage = "Unexpected error: \(error.localizedDescription)"
}
#endif


isLoading = false
}

/// Check if user has valid credentials stored
#endif

func checkAuthentication() async {
do {
let credentials = try await credentialsManager.credentials()
Expand All @@ -80,7 +103,6 @@ final class ContentViewModel: ObservableObject {
}
}


extension Array where Element: Hashable {
func uniqued() -> [Element] {
var seen = Set<Element>()
Expand Down
5 changes: 4 additions & 1 deletion App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>UIApplicationSceneManifest</key>
<dict/>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
Expand Down
9 changes: 9 additions & 0 deletions AppTests/AppTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Testing

struct AppTests {

@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}

}
Loading
Loading