diff --git a/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift b/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift
index 3af7509d2..23b9624d7 100644
--- a/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift
+++ b/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift
@@ -1,5 +1,6 @@
import SwiftUI
import AppKit
+import AuthenticationServices
import StackAuth
@main
@@ -1224,15 +1225,25 @@ struct ContactChannelsView: View {
}
}
+// MARK: - OAuth Presentation Context Provider
+
+class MacOSPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
+ func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
+ return NSApplication.shared.windows.first ?? ASPresentationAnchor()
+ }
+}
+
// MARK: - OAuth View
struct OAuthView: View {
@Bindable var viewModel: SDKTestViewModel
@State private var provider = "google"
+ @State private var isSigningIn = false
+ private let presentationProvider = MacOSPresentationContextProvider()
var body: some View {
Form {
- Section("OAuth URL Generation") {
+ Section("Sign In with OAuth") {
TextField("Provider", text: $provider)
HStack {
@@ -1241,15 +1252,65 @@ struct OAuthView: View {
Button("microsoft") { provider = "microsoft" }
}
+ Button("signInWithOAuth(provider: \"\(provider)\")") {
+ Task { await signInWithOAuth() }
+ }
+ .disabled(isSigningIn)
+
+ if isSigningIn {
+ HStack {
+ ProgressView()
+ .scaleEffect(0.7)
+ Text("Waiting for OAuth...")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+
+ Section("OAuth URL Generation (Manual)") {
Button("getOAuthUrl(provider: \"\(provider)\")") {
Task { await getOAuthUrl() }
}
+
+ Text("Returns URL, state, and codeVerifier for manual OAuth handling")
+ .font(.caption)
+ .foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.navigationTitle("OAuth")
}
+ func signInWithOAuth() async {
+ let params = "provider: \"\(provider)\""
+ viewModel.logInfo("signInWithOAuth()", message: "Opening OAuth browser...", details: params)
+ isSigningIn = true
+
+ do {
+ try await viewModel.clientApp.signInWithOAuth(
+ provider: provider,
+ presentationContextProvider: presentationProvider
+ )
+ viewModel.logCall(
+ "signInWithOAuth(provider:)",
+ params: params,
+ result: "Success! User signed in via OAuth."
+ )
+ // Fetch user to show details
+ if let user = try await viewModel.clientApp.getUser() {
+ let dict = await serializeCurrentUser(user)
+ viewModel.logCall(
+ "getUser() after OAuth",
+ result: formatObject("CurrentUser", dict)
+ )
+ }
+ } catch {
+ viewModel.logCall("signInWithOAuth(provider:)", params: params, error: error)
+ }
+
+ isSigningIn = false
+ }
+
func getOAuthUrl() async {
let params = "provider: \"\(provider)\""
viewModel.logInfo("getOAuthUrl()", message: "Calling...", details: params)
diff --git a/sdks/implementations/swift/Examples/StackAuthiOS/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/sdks/implementations/swift/Examples/StackAuthiOS/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/sdks/implementations/swift/Examples/StackAuthiOS/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/sdks/implementations/swift/Examples/StackAuthiOS/Package.swift b/sdks/implementations/swift/Examples/StackAuthiOS/Package.swift
deleted file mode 100644
index ffda99741..000000000
--- a/sdks/implementations/swift/Examples/StackAuthiOS/Package.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-// swift-tools-version: 5.9
-import PackageDescription
-
-let package = Package(
- name: "StackAuthiOS",
- platforms: [
- .iOS(.v17)
- ],
- dependencies: [
- .package(name: "StackAuth", path: "../..")
- ],
- targets: [
- .executableTarget(
- name: "StackAuthiOS",
- dependencies: [
- .product(name: "StackAuth", package: "StackAuth")
- ],
- path: "StackAuthiOS"
- )
- ]
-)
diff --git a/sdks/implementations/swift/Examples/StackAuthiOS/README.md b/sdks/implementations/swift/Examples/StackAuthiOS/README.md
index 171cbf0d3..6b425829f 100644
--- a/sdks/implementations/swift/Examples/StackAuthiOS/README.md
+++ b/sdks/implementations/swift/Examples/StackAuthiOS/README.md
@@ -1,107 +1,103 @@
# Stack Auth iOS Example
-A comprehensive iOS SwiftUI application for testing all Stack Auth SDK functions interactively.
+An interactive iOS application for testing all Stack Auth Swift SDK functions.
## Prerequisites
-- iOS 17.0+
-- Swift 5.9+
-- Xcode 15.0+
-- A running Stack Auth backend accessible from the iOS device/simulator
+- Xcode 15.0 or later
+- iOS 17.0+ Simulator or device
+- Running Stack Auth backend (default: `http://localhost:8102`)
## Running the Example
-1. Start the Stack Auth backend:
+### Option 1: Xcode
+
+1. Open the project in Xcode:
```bash
- cd /path/to/stack-2
- pnpm run dev
+ open StackAuthiOS.xcodeproj
```
-2. Open in Xcode:
- ```bash
- cd Examples/StackAuthiOS
- open Package.swift
- ```
+2. Select an iOS Simulator (e.g., "iPhone 17 Pro") as the destination
-3. Select an iOS simulator or device and run.
+3. Press ⌘R to build and run
-**Note**: When testing on a physical device, update the base URL in Settings to point to your machine's IP address (e.g., `http://192.168.1.x:8102`).
+### Option 2: Command Line
+
+```bash
+# Build
+xcodebuild -scheme StackAuthiOS -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
+
+# Build and run (opens simulator)
+xcodebuild -scheme StackAuthiOS -destination 'platform=iOS Simulator,name=iPhone 17 Pro' run
+```
## Features
-The example app uses a tab-based navigation with the following sections:
+The app uses a tab-based interface optimized for mobile:
-### Auth Tab
-- Sign up with email/password
-- Sign in with credentials
-- Sign in with wrong password (error testing)
-- Sign out
-- Get current user
-- Get user (or throw)
-- Generate OAuth URLs (Google, GitHub, Microsoft)
+- **Settings**: Configure API endpoint, project ID, and keys
+- **Auth**: Sign up, sign in, sign out, get current user
+- **User**: Update display name, metadata, view tokens
+- **Teams**: Create, list, and manage teams
+- **Logs**: View all SDK calls with full details (tap for more, long-press to copy)
-### User Tab
-- Set display name
-- Update client metadata
-- Update password (correct and wrong old password)
-- Get access/refresh tokens
-- Get auth headers
-- Get partial user from token
-- List contact channels
-
-### Teams Tab
-- Create team
-- List user's teams
-- Select and view team details
-- List team members
-- Update team name
-
-### Server Tab
-- **Users**
- - Create user (basic and with all options)
- - List users
- - Get/delete user by ID
- - Create session (impersonation)
-
-- **Teams**
- - Create team
- - List all teams
- - Add/remove users from teams
- - List team users
- - Delete team
-
-### Settings Tab
-- Configure API base URL
-- Configure project ID and API keys
-- View operation logs
-
-## Default Configuration
-
-The example is pre-configured for local development:
-- Base URL: `http://localhost:8102`
-- Project ID: `internal`
-- Publishable Key: `this-publishable-client-key-is-for-local-development-only`
-- Secret Key: `this-secret-server-key-is-for-local-development-only`
-
-## Simulator Network Notes
-
-When running in the iOS Simulator, `localhost` will connect to your Mac's localhost. For physical devices, use your Mac's local IP address.
+Additional functions are accessible via navigation links in Settings:
+- Contact Channels
+- OAuth URL generation
+- Token operations
+- Server Users (admin)
+- Server Teams (admin)
+- Sessions (impersonation)
## SDK Functions Covered
-| Category | Functions |
-|----------|-----------|
-| Auth | signUpWithCredential, signInWithCredential, signOut, getUser, getOAuthUrl |
-| User | setDisplayName, update (metadata), updatePassword, getAccessToken, getRefreshToken, getAuthHeaders, getPartialUser |
-| Teams | createTeam, listTeams, getTeam, listUsers (team members), update |
-| Contact | listContactChannels |
-| Server Users | createUser, listUsers, getUser, delete, createSession |
-| Server Teams | createTeam, listTeams, getTeam, addUser, removeUser, listUsers, delete |
-| Errors | EmailPasswordMismatchError, UserNotSignedInError, PasswordConfirmationMismatchError |
+### Client App
+- `signUpWithCredential(email:password:)`
+- `signInWithCredential(email:password:)`
+- `signOut()`
+- `getUser()` / `getUser(or:)`
+- `getAccessToken()` / `getRefreshToken()`
+- `getAuthHeaders()`
+- `getOAuthUrl(provider:)`
-## Testing Edge Cases
+### Current User
+- `setDisplayName(_:)`
+- `update(clientMetadata:)`
+- `listTeams()` / `getTeam(id:)`
+- `createTeam(displayName:)`
+- `listContactChannels()`
-The app includes buttons specifically for testing error scenarios:
-- "Sign In (Wrong Password)" - triggers EmailPasswordMismatchError
-- "Get User (or throw)" - triggers UserNotSignedInError when not signed in
-- "Update (Wrong Old Password)" - triggers PasswordConfirmationMismatchError
+### Server App
+- `createUser(email:password:...)`
+- `listUsers(limit:)`
+- `getUser(id:)`
+- `createTeam(displayName:)`
+- `listTeams()`
+- `createSession(userId:)`
+
+## Logging
+
+The Logs tab shows all SDK activity in real-time:
+- **Green checkmark**: Successful calls with full response data
+- **Red X**: Errors with details
+- **Blue info**: In-progress calls
+
+Tap any log entry to see full details. Long-press to copy to clipboard.
+
+## Network Configuration
+
+For iOS Simulator to connect to your local backend:
+
+1. The default `localhost:8102` should work in the simulator
+2. For a real device, use your computer's local IP address instead
+
+## Troubleshooting
+
+### "Could not connect to server"
+- Ensure your Stack Auth backend is running
+- Check the Base URL in Settings tab
+- For real devices, use your computer's IP instead of localhost
+
+### Build errors
+- Make sure you have Xcode 15+ installed
+- Try cleaning: Product → Clean Build Folder (⇧⌘K)
diff --git a/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS.xcodeproj/project.pbxproj b/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..851c58c6d
--- /dev/null
+++ b/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS.xcodeproj/project.pbxproj
@@ -0,0 +1,346 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ E01234560001 /* StackAuthiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E01234560002; };
+ E01234560003 /* StackAuth in Frameworks */ = {isa = PBXBuildFile; productRef = E01234560004; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ E01234560002 /* StackAuthiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackAuthiOSApp.swift; sourceTree = ""; };
+ E01234560005 /* StackAuthiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StackAuthiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ E01234560006 /* StackAuth */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = StackAuth; path = ../..; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ E01234560007 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E01234560003 /* StackAuth in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ E01234560008 = {
+ isa = PBXGroup;
+ children = (
+ E01234560009 /* StackAuthiOS */,
+ E0123456000A /* Products */,
+ E0123456000B /* Packages */,
+ );
+ sourceTree = "";
+ };
+ E01234560009 /* StackAuthiOS */ = {
+ isa = PBXGroup;
+ children = (
+ E01234560002 /* StackAuthiOSApp.swift */,
+ );
+ path = StackAuthiOS;
+ sourceTree = "";
+ };
+ E0123456000A /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ E01234560005 /* StackAuthiOS.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ E0123456000B /* Packages */ = {
+ isa = PBXGroup;
+ children = (
+ E01234560006 /* StackAuth */,
+ );
+ name = Packages;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ E0123456000C /* StackAuthiOS */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E0123456000D;
+ buildPhases = (
+ E0123456000E /* Sources */,
+ E01234560007 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = StackAuthiOS;
+ packageProductDependencies = (
+ E01234560004 /* StackAuth */,
+ );
+ productName = StackAuthiOS;
+ productReference = E01234560005 /* StackAuthiOS.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ E0123456000F /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
+ TargetAttributes = {
+ E0123456000C = {
+ CreatedOnToolsVersion = 15.0;
+ };
+ };
+ };
+ buildConfigurationList = E01234560010;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = E01234560008;
+ packageReferences = (
+ E01234560011 /* XCLocalSwiftPackageReference "../.." */,
+ );
+ productRefGroup = E0123456000A /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ E0123456000C /* StackAuthiOS */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXSourcesBuildPhase section */
+ E0123456000E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E01234560001 /* StackAuthiOSApp.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ E01234560012 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ E01234560013 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ E01234560014 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.stackauth.example.ios;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ E01234560015 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.stackauth.example.ios;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ E0123456000D /* Build configuration list for PBXNativeTarget "StackAuthiOS" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E01234560014 /* Debug */,
+ E01234560015 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E01234560010 /* Build configuration list for PBXProject "StackAuthiOS" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E01234560012 /* Debug */,
+ E01234560013 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ E01234560011 /* XCLocalSwiftPackageReference "../.." */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = "../..";
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ E01234560004 /* StackAuth */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = StackAuth;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = E0123456000F /* Project object */;
+}
diff --git a/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift b/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift
index 1e1fefbe2..e6d381b2b 100644
--- a/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift
+++ b/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift
@@ -1,4 +1,6 @@
import SwiftUI
+import UIKit
+import AuthenticationServices
import StackAuth
@main
@@ -10,51 +12,343 @@ struct StackAuthiOSApp: App {
}
}
+// MARK: - iOS OAuth Presentation Context Provider
+
+class iOSPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
+ func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
+ guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let window = scene.windows.first else {
+ return ASPresentationAnchor()
+ }
+ return window
+ }
+}
+
// MARK: - Main Content View
struct ContentView: View {
@State private var viewModel = SDKTestViewModel()
+ @State private var selectedTab = 0
+ @State private var lastSeenLogCount = 0
+
+ var unreadLogCount: Int {
+ max(0, viewModel.logs.count - lastSeenLogCount)
+ }
var body: some View {
- TabView {
- NavigationStack {
- AuthenticationView(viewModel: viewModel)
+ ZStack {
+ TabView(selection: $selectedTab) {
+ NavigationStack {
+ SettingsView(viewModel: viewModel)
+ }
+ .tabItem {
+ Label("Settings", systemImage: "gear")
+ }
+ .tag(0)
+
+ NavigationStack {
+ AuthenticationView(viewModel: viewModel)
+ }
+ .tabItem {
+ Label("Auth", systemImage: "person.badge.key")
+ }
+ .tag(1)
+
+ NavigationStack {
+ UserManagementView(viewModel: viewModel)
+ }
+ .tabItem {
+ Label("User", systemImage: "person.crop.circle")
+ }
+ .tag(2)
+
+ NavigationStack {
+ TeamsView(viewModel: viewModel)
+ }
+ .tabItem {
+ Label("Teams", systemImage: "person.3")
+ }
+ .tag(3)
+
+ NavigationStack {
+ LogsView(viewModel: viewModel)
+ }
+ .tabItem {
+ Label("Logs", systemImage: "list.bullet.rectangle")
+ }
+ .badge(unreadLogCount > 0 ? unreadLogCount : 0)
+ .tag(4)
}
- .tabItem {
- Label("Auth", systemImage: "person.badge.key")
+ .onChange(of: selectedTab) { _, newTab in
+ if newTab == 4 {
+ // User switched to Logs tab, mark all as read
+ lastSeenLogCount = viewModel.logs.count
+ }
}
- NavigationStack {
- UserView(viewModel: viewModel)
+ // Toast notification overlay
+ LogToastView(viewModel: viewModel, selectedTab: $selectedTab)
+ }
+ }
+}
+
+// MARK: - Log Toast View
+
+struct LogToastView: View {
+ @Bindable var viewModel: SDKTestViewModel
+ @Binding var selectedTab: Int
+ @State private var showToast = false
+ @State private var toastEntry: LogEntry?
+ @State private var lastLogId: UUID?
+
+ var body: some View {
+ VStack {
+ if showToast, let entry = toastEntry, selectedTab != 4 {
+ HStack(spacing: 12) {
+ Image(systemName: entry.type.icon)
+ .foregroundStyle(entry.type.color)
+
+ VStack(alignment: .leading, spacing: 2) {
+ if let function = entry.function {
+ Text(function)
+ .font(.caption.bold())
+ .lineLimit(1)
+ }
+ Text(entry.message)
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ .lineLimit(2)
+ }
+
+ Spacer()
+
+ Button {
+ selectedTab = 4
+ withAnimation {
+ showToast = false
+ }
+ } label: {
+ Text("View")
+ .font(.caption.bold())
+ }
+ .buttonStyle(.borderedProminent)
+ .buttonBorderShape(.capsule)
+ .controlSize(.small)
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
+ .shadow(radius: 8)
+ .padding(.horizontal)
+ .transition(.move(edge: .top).combined(with: .opacity))
+ .onTapGesture {
+ selectedTab = 4
+ withAnimation {
+ showToast = false
+ }
+ }
}
- .tabItem {
- Label("User", systemImage: "person.crop.circle")
+ Spacer()
+ }
+ .padding(.top, 8)
+ .onChange(of: viewModel.logs.first?.id) { _, newId in
+ guard let newId = newId, newId != lastLogId, selectedTab != 4 else { return }
+ lastLogId = newId
+ toastEntry = viewModel.logs.first
+ withAnimation(.spring(duration: 0.3)) {
+ showToast = true
}
-
- NavigationStack {
- TeamsView(viewModel: viewModel)
- }
- .tabItem {
- Label("Teams", systemImage: "person.3")
- }
-
- NavigationStack {
- ServerView(viewModel: viewModel)
- }
- .tabItem {
- Label("Server", systemImage: "server.rack")
- }
-
- NavigationStack {
- SettingsView(viewModel: viewModel)
- }
- .tabItem {
- Label("Settings", systemImage: "gear")
+ // Auto-hide after 3 seconds
+ Task {
+ try? await Task.sleep(for: .seconds(3))
+ withAnimation {
+ if toastEntry?.id == newId {
+ showToast = false
+ }
+ }
}
}
}
}
+// MARK: - Logs View
+
+struct LogsView: View {
+ @Bindable var viewModel: SDKTestViewModel
+ @State private var selectedLogId: UUID?
+
+ var body: some View {
+ VStack(spacing: 0) {
+ if viewModel.logs.isEmpty {
+ VStack {
+ Spacer()
+ Image(systemName: "list.bullet.rectangle")
+ .font(.system(size: 48))
+ .foregroundStyle(.tertiary)
+ Text("No activity yet")
+ .foregroundStyle(.secondary)
+ Text("Use the SDK from other tabs to see logs here")
+ .font(.caption)
+ .foregroundStyle(.tertiary)
+ Spacer()
+ }
+ } else {
+ List(viewModel.logs, selection: $selectedLogId) { entry in
+ LogEntryView(entry: entry)
+ .id(entry.id)
+ .contextMenu {
+ Button {
+ UIPasteboard.general.string = entry.message
+ } label: {
+ Label("Copy Message", systemImage: "doc.on.doc")
+ }
+ Button {
+ UIPasteboard.general.string = entry.fullDescription
+ } label: {
+ Label("Copy Full Details", systemImage: "doc.on.doc.fill")
+ }
+ }
+ }
+ .listStyle(.plain)
+ }
+ }
+ .navigationTitle("SDK Logs")
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ HStack {
+ Text("\(viewModel.logs.count)")
+ .foregroundStyle(.secondary)
+ .font(.caption)
+ Button("Clear") {
+ viewModel.clearLogs()
+ }
+ }
+ }
+ }
+ .sheet(item: $selectedLogId) { id in
+ if let entry = viewModel.logs.first(where: { $0.id == id }) {
+ LogDetailSheet(entry: entry)
+ }
+ }
+ }
+}
+
+extension UUID: @retroactive Identifiable {
+ public var id: UUID { self }
+}
+
+struct LogDetailSheet: View {
+ let entry: LogEntry
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ Image(systemName: entry.type.icon)
+ .foregroundStyle(entry.type.color)
+ Text(entry.type.rawValue)
+ .font(.headline)
+ .foregroundStyle(entry.type.color)
+ Spacer()
+ Text(entry.timestamp, style: .time)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ if let function = entry.function {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Function")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Text(function)
+ .font(.system(.body, design: .monospaced))
+ }
+ }
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Details")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Text(entry.fullDescription)
+ .font(.system(.caption, design: .monospaced))
+ .textSelection(.enabled)
+ }
+ }
+ .padding()
+ }
+ .navigationTitle("Log Entry")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button("Done") {
+ dismiss()
+ }
+ }
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ UIPasteboard.general.string = entry.fullDescription
+ } label: {
+ Image(systemName: "doc.on.doc")
+ }
+ }
+ }
+ }
+ }
+}
+
+struct LogEntryView: View {
+ let entry: LogEntry
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack(alignment: .top) {
+ Image(systemName: entry.type.icon)
+ .foregroundStyle(entry.type.color)
+ .frame(width: 20)
+
+ VStack(alignment: .leading, spacing: 2) {
+ if let function = entry.function {
+ Text(function)
+ .font(.system(.caption, design: .monospaced).bold())
+ .foregroundStyle(.primary)
+ }
+
+ Text(entry.message)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(entry.type.color)
+ .lineLimit(3)
+
+ Text(entry.timestamp, style: .time)
+ .font(.caption2)
+ .foregroundStyle(.tertiary)
+ }
+
+ Spacer()
+ }
+ }
+ .padding(.vertical, 4)
+ }
+}
+
+// MARK: - Test Sections
+
+enum TestSection: String, CaseIterable, Identifiable {
+ case settings
+ case authentication
+ case userManagement
+ case teams
+ case contactChannels
+ case oauth
+ case tokens
+ case serverUsers
+ case serverTeams
+ case sessions
+
+ var id: String { rawValue }
+}
+
// MARK: - View Model
@Observable
@@ -66,7 +360,9 @@ class SDKTestViewModel {
var secretServerKey = "this-secret-server-key-is-for-local-development-only"
// State
+ var selectedSection: TestSection = .settings
var logs: [LogEntry] = []
+ var isLoading = false
// Apps (lazy initialized)
private var _clientApp: StackClientApp?
@@ -100,14 +396,54 @@ class SDKTestViewModel {
func resetApps() {
_clientApp = nil
_serverApp = nil
- log("Apps reset with new configuration", type: .info)
+ logCall("resetApps()", result: "Apps reset with new configuration")
}
- func log(_ message: String, type: LogType = .info) {
- let entry = LogEntry(message: message, type: type, timestamp: Date())
+ // Enhanced logging
+ func logCall(_ function: String, params: String? = nil, result: String) {
+ let message = result
+ let details = params.map { "Parameters:\n\($0)\n\nResult:\n\(result)" } ?? "Result:\n\(result)"
+ let entry = LogEntry(
+ function: function,
+ message: message,
+ details: details,
+ type: .success,
+ timestamp: Date()
+ )
logs.insert(entry, at: 0)
- if logs.count > 50 {
- logs.removeLast()
+ trimLogs()
+ }
+
+ func logCall(_ function: String, params: String? = nil, error: Error) {
+ let errorStr = String(describing: error)
+ let message = errorStr
+ let details = params.map { "Parameters:\n\($0)\n\nError:\n\(errorStr)" } ?? "Error:\n\(errorStr)"
+ let entry = LogEntry(
+ function: function,
+ message: message,
+ details: details,
+ type: .error,
+ timestamp: Date()
+ )
+ logs.insert(entry, at: 0)
+ trimLogs()
+ }
+
+ func logInfo(_ function: String, message: String, details: String? = nil) {
+ let entry = LogEntry(
+ function: function,
+ message: message,
+ details: details ?? message,
+ type: .info,
+ timestamp: Date()
+ )
+ logs.insert(entry, at: 0)
+ trimLogs()
+ }
+
+ private func trimLogs() {
+ if logs.count > 200 {
+ logs.removeLast(logs.count - 200)
}
}
@@ -118,13 +454,31 @@ class SDKTestViewModel {
struct LogEntry: Identifiable {
let id = UUID()
+ let function: String?
let message: String
+ let details: String?
let type: LogType
let timestamp: Date
+
+ var fullDescription: String {
+ var parts: [String] = []
+ parts.append("Time: \(timestamp.formatted(date: .omitted, time: .standard))")
+ if let function = function {
+ parts.append("Function: \(function)")
+ }
+ parts.append("Status: \(type.rawValue)")
+ parts.append("Message: \(message)")
+ if let details = details {
+ parts.append("\nDetails:\n\(details)")
+ }
+ return parts.joined(separator: "\n")
+ }
}
-enum LogType {
- case info, success, error
+enum LogType: String {
+ case info = "INFO"
+ case success = "SUCCESS"
+ case error = "ERROR"
var color: Color {
switch self {
@@ -133,6 +487,174 @@ enum LogType {
case .error: return .red
}
}
+
+ var icon: String {
+ switch self {
+ case .info: return "info.circle"
+ case .success: return "checkmark.circle.fill"
+ case .error: return "xmark.circle.fill"
+ }
+ }
+}
+
+// MARK: - Object Serialization Helpers
+
+func formatValue(_ value: Any?, indent: Int = 0) -> String {
+ let spaces = String(repeating: " ", count: indent)
+
+ guard let value = value else { return "nil" }
+
+ switch value {
+ case let str as String:
+ return "\"\(str)\""
+ case let bool as Bool:
+ return bool ? "true" : "false"
+ case let num as NSNumber:
+ return "\(num)"
+ case let date as Date:
+ return "\"\(date.formatted())\""
+ case let url as URL:
+ return "\"\(url.absoluteString)\""
+ case let dict as [String: Any]:
+ if dict.isEmpty { return "{}" }
+ var lines = ["{"]
+ for (key, val) in dict.sorted(by: { $0.key < $1.key }) {
+ lines.append("\(spaces) \(key): \(formatValue(val, indent: indent + 1))")
+ }
+ lines.append("\(spaces)}")
+ return lines.joined(separator: "\n")
+ case let arr as [Any]:
+ if arr.isEmpty { return "[]" }
+ var lines = ["["]
+ for item in arr {
+ lines.append("\(spaces) \(formatValue(item, indent: indent + 1)),")
+ }
+ lines.append("\(spaces)]")
+ return lines.joined(separator: "\n")
+ default:
+ return String(describing: value)
+ }
+}
+
+func serializeCurrentUser(_ user: CurrentUser) async -> [String: Any] {
+ var dict: [String: Any] = [:]
+ dict["id"] = await user.id
+ dict["displayName"] = await user.displayName
+ dict["primaryEmail"] = await user.primaryEmail
+ dict["primaryEmailVerified"] = await user.primaryEmailVerified
+ dict["profileImageUrl"] = await user.profileImageUrl
+ dict["signedUpAt"] = await user.signedUpAt.formatted()
+ dict["clientMetadata"] = await user.clientMetadata
+ dict["clientReadOnlyMetadata"] = await user.clientReadOnlyMetadata
+ dict["hasPassword"] = await user.hasPassword
+ dict["emailAuthEnabled"] = await user.emailAuthEnabled
+ dict["otpAuthEnabled"] = await user.otpAuthEnabled
+ dict["passkeyAuthEnabled"] = await user.passkeyAuthEnabled
+ dict["isMultiFactorRequired"] = await user.isMultiFactorRequired
+ dict["isAnonymous"] = await user.isAnonymous
+ dict["isRestricted"] = await user.isRestricted
+ if let reason = await user.restrictedReason {
+ dict["restrictedReason"] = String(describing: reason)
+ }
+ let providers = await user.oauthProviders
+ if !providers.isEmpty {
+ dict["oauthProviders"] = providers.map { ["id": $0.id] }
+ }
+ if let team = await user.selectedTeam {
+ dict["selectedTeam"] = ["id": team.id, "displayName": await team.displayName]
+ }
+ return dict
+}
+
+func serializeServerUser(_ user: ServerUser) async -> [String: Any] {
+ var dict: [String: Any] = [:]
+ dict["id"] = user.id
+ dict["displayName"] = await user.displayName
+ dict["primaryEmail"] = await user.primaryEmail
+ dict["primaryEmailVerified"] = await user.primaryEmailVerified
+ dict["profileImageUrl"] = await user.profileImageUrl
+ dict["signedUpAt"] = await user.signedUpAt.formatted()
+ if let lastActiveAt = await user.lastActiveAt {
+ dict["lastActiveAt"] = lastActiveAt.formatted()
+ }
+ dict["clientMetadata"] = await user.clientMetadata
+ dict["clientReadOnlyMetadata"] = await user.clientReadOnlyMetadata
+ dict["serverMetadata"] = await user.serverMetadata
+ dict["hasPassword"] = await user.hasPassword
+ dict["emailAuthEnabled"] = await user.emailAuthEnabled
+ dict["otpAuthEnabled"] = await user.otpAuthEnabled
+ dict["passkeyAuthEnabled"] = await user.passkeyAuthEnabled
+ dict["isMultiFactorRequired"] = await user.isMultiFactorRequired
+ return dict
+}
+
+func serializeTeam(_ team: Team) async -> [String: Any] {
+ var dict: [String: Any] = [:]
+ dict["id"] = team.id
+ dict["displayName"] = await team.displayName
+ dict["profileImageUrl"] = await team.profileImageUrl
+ dict["clientMetadata"] = await team.clientMetadata
+ dict["clientReadOnlyMetadata"] = await team.clientReadOnlyMetadata
+ return dict
+}
+
+func serializeServerTeam(_ team: ServerTeam) async -> [String: Any] {
+ var dict: [String: Any] = [:]
+ dict["id"] = team.id
+ dict["displayName"] = await team.displayName
+ dict["profileImageUrl"] = await team.profileImageUrl
+ dict["clientMetadata"] = await team.clientMetadata
+ dict["clientReadOnlyMetadata"] = await team.clientReadOnlyMetadata
+ dict["serverMetadata"] = await team.serverMetadata
+ dict["createdAt"] = await team.createdAt.formatted()
+ return dict
+}
+
+func serializeContactChannel(_ channel: ContactChannel) async -> [String: Any] {
+ var dict: [String: Any] = [:]
+ dict["id"] = channel.id
+ dict["type"] = await channel.type
+ dict["value"] = await channel.value
+ dict["isPrimary"] = await channel.isPrimary
+ dict["isVerified"] = await channel.isVerified
+ dict["usedForAuth"] = await channel.usedForAuth
+ return dict
+}
+
+func serializeTeamUser(_ user: TeamUser) -> [String: Any] {
+ var dict: [String: Any] = [:]
+ dict["id"] = user.id
+ dict["teamProfile"] = [
+ "displayName": user.teamProfile.displayName as Any,
+ "profileImageUrl": user.teamProfile.profileImageUrl as Any
+ ]
+ return dict
+}
+
+func formatObject(_ name: String, _ dict: [String: Any]) -> String {
+ var lines = ["\(name) {"]
+ for (key, value) in dict.sorted(by: { $0.key < $1.key }) {
+ lines.append(" \(key): \(formatValue(value, indent: 1))")
+ }
+ lines.append("}")
+ return lines.joined(separator: "\n")
+}
+
+func formatObjectArray(_ name: String, _ items: [[String: Any]]) -> String {
+ if items.isEmpty {
+ return "\(name) []"
+ }
+ var lines = ["\(name) ["]
+ for (index, item) in items.enumerated() {
+ lines.append(" [\(index)] {")
+ for (key, value) in item.sorted(by: { $0.key < $1.key }) {
+ lines.append(" \(key): \(formatValue(value, indent: 2))")
+ }
+ lines.append(" }")
+ }
+ lines.append("]")
+ lines.append("Total: \(items.count) items")
+ return lines.joined(separator: "\n")
}
// MARK: - Settings View
@@ -141,43 +663,65 @@ struct SettingsView: View {
@Bindable var viewModel: SDKTestViewModel
var body: some View {
- List {
+ Form {
Section("API Configuration") {
TextField("Base URL", text: $viewModel.baseUrl)
.textInputAutocapitalization(.never)
- .autocorrectionDisabled()
+ .keyboardType(.URL)
TextField("Project ID", text: $viewModel.projectId)
.textInputAutocapitalization(.never)
- .autocorrectionDisabled()
TextField("Publishable Client Key", text: $viewModel.publishableClientKey)
.textInputAutocapitalization(.never)
- .autocorrectionDisabled()
SecureField("Secret Server Key", text: $viewModel.secretServerKey)
Button("Apply Configuration") {
viewModel.resetApps()
}
+ .buttonStyle(.borderedProminent)
}
- Section("Logs (\(viewModel.logs.count))") {
- Button("Clear Logs") {
- viewModel.clearLogs()
+ Section("Quick Actions") {
+ Button("Test Connection") {
+ Task { await testConnection() }
}
-
- ForEach(viewModel.logs) { entry in
- VStack(alignment: .leading) {
- Text(entry.timestamp, style: .time)
- .font(.caption2)
- .foregroundStyle(.secondary)
- Text(entry.message)
- .font(.system(.caption, design: .monospaced))
- .foregroundStyle(entry.type.color)
- }
+ }
+
+ Section("More Functions") {
+ NavigationLink("Contact Channels") {
+ ContactChannelsView(viewModel: viewModel)
+ }
+ NavigationLink("OAuth") {
+ OAuthView(viewModel: viewModel)
+ }
+ NavigationLink("Tokens") {
+ TokensView(viewModel: viewModel)
+ }
+ NavigationLink("Server Users") {
+ ServerUsersView(viewModel: viewModel)
+ }
+ NavigationLink("Server Teams") {
+ ServerTeamsView(viewModel: viewModel)
+ }
+ NavigationLink("Sessions") {
+ SessionsView(viewModel: viewModel)
}
}
}
.navigationTitle("Settings")
}
+
+ func testConnection() async {
+ viewModel.logInfo("testConnection()", message: "Testing connection to \(viewModel.baseUrl)...")
+ do {
+ let project = try await viewModel.clientApp.getProject()
+ viewModel.logCall(
+ "getProject()",
+ result: "Connected! Project ID: \(project.id)"
+ )
+ } catch {
+ viewModel.logCall("getProject()", error: error)
+ }
+ }
}
// MARK: - Authentication View
@@ -186,193 +730,191 @@ struct AuthenticationView: View {
@Bindable var viewModel: SDKTestViewModel
@State private var email = ""
@State private var password = "TestPassword123!"
- @State private var currentUserEmail: String?
- @State private var currentUserId: String?
+ @State private var currentUser: String?
var body: some View {
- List {
+ Form {
Section("Credentials") {
TextField("Email", text: $email)
.textInputAutocapitalization(.never)
- .autocorrectionDisabled()
.keyboardType(.emailAddress)
SecureField("Password", text: $password)
Button("Generate Random Email") {
- email = "test-\(UUID().uuidString.lowercased().prefix(8))@example.com"
+ email = "test-\(UUID().uuidString.lowercased())@example.com"
+ viewModel.logInfo("generateEmail()", message: "Generated: \(email)")
}
}
- Section("Actions") {
- Button("Sign Up") {
+ Section("Sign Up") {
+ Button("signUpWithCredential(email, password)") {
Task { await signUp() }
}
.disabled(email.isEmpty || password.isEmpty)
-
- Button("Sign In") {
+ }
+
+ Section("Sign In") {
+ Button("signInWithCredential(email, password)") {
Task { await signIn() }
}
.disabled(email.isEmpty || password.isEmpty)
- Button("Sign In (Wrong Password)") {
+ Button("signInWithCredential(email, WRONG_PASSWORD)") {
Task { await signInWrongPassword() }
}
.disabled(email.isEmpty)
-
- Button("Sign Out") {
+ }
+
+ Section("Sign Out") {
+ Button("signOut()") {
Task { await signOut() }
}
}
Section("Current User") {
- Button("Refresh User") {
+ Button("getUser()") {
Task { await getUser() }
}
- if let email = currentUserEmail, let id = currentUserId {
- Text("Email: \(email)")
- Text("ID: \(id)")
- .font(.caption)
- .foregroundStyle(.secondary)
- } else {
- Text("Not signed in")
- .foregroundStyle(.secondary)
- }
- }
-
- Section("OAuth") {
- Button("Get Google OAuth URL") {
- Task { await getOAuthUrl("google") }
- }
- Button("Get GitHub OAuth URL") {
- Task { await getOAuthUrl("github") }
- }
- Button("Get Microsoft OAuth URL") {
- Task { await getOAuthUrl("microsoft") }
- }
- }
-
- Section("Error Testing") {
- Button("Get User (or throw)") {
+ Button("getUser(or: .throw)") {
Task { await getUserOrThrow() }
}
+
+ if let user = currentUser {
+ Text(user)
+ .font(.system(.body, design: .monospaced))
+ .foregroundStyle(.secondary)
+ }
}
}
.navigationTitle("Authentication")
- .onAppear {
- Task { await getUser() }
- }
}
func signUp() async {
+ let params = "email: \"\(email)\"\npassword: \"\(password)\""
+ viewModel.logInfo("signUpWithCredential()", message: "Calling...", details: params)
+
do {
- viewModel.log("Signing up: \(email)")
try await viewModel.clientApp.signUpWithCredential(email: email, password: password)
- viewModel.log("Sign up successful!", type: .success)
+ viewModel.logCall(
+ "signUpWithCredential(email, password)",
+ params: params,
+ result: "Success! User signed up."
+ )
await getUser()
} catch {
- viewModel.log("Sign up failed: \(error)", type: .error)
+ viewModel.logCall("signUpWithCredential(email, password)", params: params, error: error)
}
}
func signIn() async {
+ let params = "email: \"\(email)\"\npassword: \"\(password)\""
+ viewModel.logInfo("signInWithCredential()", message: "Calling...", details: params)
+
do {
- viewModel.log("Signing in: \(email)")
try await viewModel.clientApp.signInWithCredential(email: email, password: password)
- viewModel.log("Sign in successful!", type: .success)
+ viewModel.logCall(
+ "signInWithCredential(email, password)",
+ params: params,
+ result: "Success! User signed in."
+ )
await getUser()
} catch {
- viewModel.log("Sign in failed: \(error)", type: .error)
+ viewModel.logCall("signInWithCredential(email, password)", params: params, error: error)
}
}
func signInWrongPassword() async {
+ let params = "email: \"\(email)\"\npassword: \"WrongPassword!\""
+ viewModel.logInfo("signInWithCredential()", message: "Calling with wrong password...", details: params)
+
do {
- viewModel.log("Signing in with wrong password...")
try await viewModel.clientApp.signInWithCredential(email: email, password: "WrongPassword!")
- viewModel.log("Sign in succeeded (unexpected)", type: .error)
+ viewModel.logCall(
+ "signInWithCredential(email, WRONG)",
+ params: params,
+ result: "Unexpected success (should have failed)"
+ )
} catch let error as EmailPasswordMismatchError {
- viewModel.log("Got EmailPasswordMismatchError: \(error.message)", type: .success)
+ viewModel.logCall(
+ "signInWithCredential(email, WRONG)",
+ params: params,
+ result: "Expected error caught!\nType: EmailPasswordMismatchError\nCode: \(error.code)\nMessage: \(error.message)"
+ )
} catch {
- viewModel.log("Unexpected error: \(error)", type: .error)
+ viewModel.logCall("signInWithCredential(email, WRONG)", params: params, error: error)
}
}
func signOut() async {
+ viewModel.logInfo("signOut()", message: "Calling...")
+
do {
- viewModel.log("Signing out...")
try await viewModel.clientApp.signOut()
- viewModel.log("Sign out successful!", type: .success)
- currentUserEmail = nil
- currentUserId = nil
+ viewModel.logCall("signOut()", result: "Success! User signed out.")
+ currentUser = nil
} catch {
- viewModel.log("Sign out failed: \(error)", type: .error)
+ viewModel.logCall("signOut()", error: error)
}
}
func getUser() async {
+ viewModel.logInfo("getUser()", message: "Calling...")
+
do {
let user = try await viewModel.clientApp.getUser()
if let user = user {
- currentUserEmail = await user.primaryEmail
- currentUserId = await user.id
- viewModel.log("Got user: \(currentUserEmail ?? "nil")", type: .success)
+ let dict = await serializeCurrentUser(user)
+ currentUser = "ID: \(dict["id"] ?? "")\nEmail: \(dict["primaryEmail"] ?? "nil")"
+ viewModel.logCall(
+ "getUser()",
+ result: formatObject("CurrentUser", dict)
+ )
} else {
- currentUserEmail = nil
- currentUserId = nil
- viewModel.log("No user signed in", type: .info)
+ currentUser = nil
+ viewModel.logCall("getUser()", result: "nil (no user signed in)")
}
} catch {
- viewModel.log("Get user failed: \(error)", type: .error)
+ viewModel.logCall("getUser()", error: error)
}
}
func getUserOrThrow() async {
+ viewModel.logInfo("getUser(or: .throw)", message: "Calling...")
+
do {
- viewModel.log("Getting user (or throw)...")
let user = try await viewModel.clientApp.getUser(or: .throw)
if let user = user {
- let email = await user.primaryEmail
- viewModel.log("Got user: \(email ?? "nil")", type: .success)
+ let dict = await serializeCurrentUser(user)
+ viewModel.logCall("getUser(or: .throw)", result: formatObject("CurrentUser", dict))
} else {
- viewModel.log("No user (unexpected with .throw)", type: .error)
+ viewModel.logCall("getUser(or: .throw)", result: "nil (unexpected)")
}
} catch let error as UserNotSignedInError {
- viewModel.log("Got UserNotSignedInError: \(error.message)", type: .success)
+ viewModel.logCall(
+ "getUser(or: .throw)",
+ result: "Expected error caught!\nType: UserNotSignedInError\nCode: \(error.code)\nMessage: \(error.message)"
+ )
} catch {
- viewModel.log("Unexpected error: \(error)", type: .error)
- }
- }
-
- func getOAuthUrl(_ provider: String) async {
- do {
- viewModel.log("Getting OAuth URL for \(provider)...")
- let result = try await viewModel.clientApp.getOAuthUrl(provider: provider)
- viewModel.log("URL: \(result.url)", type: .success)
- viewModel.log("State: \(result.state.prefix(20))...", type: .info)
- } catch {
- viewModel.log("Get OAuth URL failed: \(error)", type: .error)
+ viewModel.logCall("getUser(or: .throw)", error: error)
}
}
}
-// MARK: - User View
+// MARK: - User Management View
-struct UserView: View {
+struct UserManagementView: View {
@Bindable var viewModel: SDKTestViewModel
@State private var displayName = ""
@State private var metadataKey = "theme"
@State private var metadataValue = "dark"
- @State private var oldPassword = "TestPassword123!"
- @State private var newPassword = "NewPassword456!"
- @State private var channels: [(id: String, value: String, isPrimary: Bool)] = []
var body: some View {
- List {
+ Form {
Section("Display Name") {
TextField("Display Name", text: $displayName)
- Button("Set Display Name") {
+ Button("user.setDisplayName(displayName)") {
Task { await setDisplayName() }
}
.disabled(displayName.isEmpty)
@@ -382,41 +924,276 @@ struct UserView: View {
TextField("Key", text: $metadataKey)
TextField("Value", text: $metadataValue)
- Button("Update Metadata") {
+ Button("user.update(clientMetadata: {key: value})") {
Task { await updateMetadata() }
}
}
- Section("Password") {
- SecureField("Old Password", text: $oldPassword)
- SecureField("New Password", text: $newPassword)
-
- Button("Update Password") {
- Task { await updatePassword() }
- }
-
- Button("Update (Wrong Old Password)") {
- Task { await updatePasswordWrong() }
- }
- }
-
- Section("Tokens") {
- Button("Get Access Token") {
+ Section("Token Info") {
+ Button("getAccessToken()") {
Task { await getAccessToken() }
}
- Button("Get Refresh Token") {
+
+ Button("getRefreshToken()") {
Task { await getRefreshToken() }
}
- Button("Get Auth Headers") {
+
+ Button("getAuthHeaders()") {
Task { await getAuthHeaders() }
}
- Button("Get Partial User") {
- Task { await getPartialUser() }
+ }
+ }
+ .navigationTitle("User Management")
+ }
+
+ func setDisplayName() async {
+ let params = "displayName: \"\(displayName)\""
+ viewModel.logInfo("setDisplayName()", message: "Calling...", details: params)
+
+ do {
+ guard let user = try await viewModel.clientApp.getUser() else {
+ viewModel.logCall("setDisplayName()", result: "Error: No user signed in")
+ return
+ }
+ try await user.setDisplayName(displayName)
+ let dict = await serializeCurrentUser(user)
+ viewModel.logCall(
+ "user.setDisplayName(displayName)",
+ params: params,
+ result: "Success!\n\n" + formatObject("CurrentUser (updated)", dict)
+ )
+ } catch {
+ viewModel.logCall("user.setDisplayName(displayName)", params: params, error: error)
+ }
+ }
+
+ func updateMetadata() async {
+ let params = "clientMetadata: {\"\(metadataKey)\": \"\(metadataValue)\"}"
+ viewModel.logInfo("update(clientMetadata:)", message: "Calling...", details: params)
+
+ do {
+ guard let user = try await viewModel.clientApp.getUser() else {
+ viewModel.logCall("update(clientMetadata:)", result: "Error: No user signed in")
+ return
+ }
+ try await user.update(clientMetadata: [metadataKey: metadataValue])
+ let dict = await serializeCurrentUser(user)
+ viewModel.logCall(
+ "user.update(clientMetadata:)",
+ params: params,
+ result: "Success!\n\n" + formatObject("CurrentUser (updated)", dict)
+ )
+ } catch {
+ viewModel.logCall("user.update(clientMetadata:)", params: params, error: error)
+ }
+ }
+
+ func getAccessToken() async {
+ viewModel.logInfo("getAccessToken()", message: "Calling...")
+
+ let token = await viewModel.clientApp.getAccessToken()
+ if let token = token {
+ let parts = token.split(separator: ".")
+ viewModel.logCall(
+ "getAccessToken()",
+ result: "JWT Token (\(parts.count) parts, \(token.count) chars):\n\(token)"
+ )
+ } else {
+ viewModel.logCall("getAccessToken()", result: "nil (not signed in)")
+ }
+ }
+
+ func getRefreshToken() async {
+ viewModel.logInfo("getRefreshToken()", message: "Calling...")
+
+ let token = await viewModel.clientApp.getRefreshToken()
+ if let token = token {
+ viewModel.logCall(
+ "getRefreshToken()",
+ result: "Refresh Token (\(token.count) chars):\n\(token)"
+ )
+ } else {
+ viewModel.logCall("getRefreshToken()", result: "nil (not signed in)")
+ }
+ }
+
+ func getAuthHeaders() async {
+ viewModel.logInfo("getAuthHeaders()", message: "Calling...")
+
+ let headers = await viewModel.clientApp.getAuthHeaders()
+ var result = "Headers:\n"
+ for (key, value) in headers {
+ result += " \(key): \(value)\n"
+ }
+ viewModel.logCall("getAuthHeaders()", result: result)
+ }
+}
+
+// MARK: - Teams View
+
+struct TeamsView: View {
+ @Bindable var viewModel: SDKTestViewModel
+ @State private var teamName = ""
+ @State private var teams: [(id: String, name: String)] = []
+ @State private var selectedTeamId = ""
+
+ var body: some View {
+ Form {
+ Section("Create Team") {
+ TextField("Team Name", text: $teamName)
+
+ Button("Generate Random Name") {
+ teamName = "Team \(UUID().uuidString.prefix(8))"
+ viewModel.logInfo("generateTeamName()", message: "Generated: \(teamName)")
+ }
+
+ Button("user.createTeam(displayName: teamName)") {
+ Task { await createTeam() }
+ }
+ .disabled(teamName.isEmpty)
+ }
+
+ Section("List Teams") {
+ Button("user.listTeams()") {
+ Task { await listTeams() }
+ }
+
+ ForEach(teams, id: \.id) { team in
+ HStack {
+ Text(team.name)
+ Spacer()
+ Text(team.id.prefix(8) + "...")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Button("Select") {
+ selectedTeamId = team.id
+ viewModel.logInfo("selectTeam()", message: "Selected team: \(team.id)")
+ }
+ .buttonStyle(.borderless)
+ }
}
}
+ Section("Team Operations") {
+ TextField("Team ID", text: $selectedTeamId)
+ .textInputAutocapitalization(.never)
+
+ Button("user.getTeam(id: teamId)") {
+ Task { await getTeam() }
+ }
+ .disabled(selectedTeamId.isEmpty)
+
+ Button("team.listUsers()") {
+ Task { await listTeamMembers() }
+ }
+ .disabled(selectedTeamId.isEmpty)
+ }
+ }
+ .navigationTitle("Teams")
+ }
+
+ func createTeam() async {
+ let params = "displayName: \"\(teamName)\""
+ viewModel.logInfo("createTeam()", message: "Calling...", details: params)
+
+ do {
+ guard let user = try await viewModel.clientApp.getUser() else {
+ viewModel.logCall("createTeam()", result: "Error: No user signed in")
+ return
+ }
+ let team = try await user.createTeam(displayName: teamName)
+ let dict = await serializeTeam(team)
+ viewModel.logCall(
+ "user.createTeam(displayName:)",
+ params: params,
+ result: formatObject("Team", dict)
+ )
+ await listTeams()
+ } catch {
+ viewModel.logCall("user.createTeam(displayName:)", params: params, error: error)
+ }
+ }
+
+ func listTeams() async {
+ viewModel.logInfo("listTeams()", message: "Calling...")
+
+ do {
+ guard let user = try await viewModel.clientApp.getUser() else {
+ viewModel.logCall("listTeams()", result: "Error: No user signed in")
+ return
+ }
+ let teamsList = try await user.listTeams()
+ var results: [(id: String, name: String)] = []
+ var dicts: [[String: Any]] = []
+ for team in teamsList {
+ let dict = await serializeTeam(team)
+ dicts.append(dict)
+ results.append((id: team.id, name: dict["displayName"] as? String ?? ""))
+ }
+ teams = results
+ viewModel.logCall("user.listTeams()", result: formatObjectArray("Team", dicts))
+ } catch {
+ viewModel.logCall("user.listTeams()", error: error)
+ }
+ }
+
+ func getTeam() async {
+ let params = "id: \"\(selectedTeamId)\""
+ viewModel.logInfo("getTeam()", message: "Calling...", details: params)
+
+ do {
+ guard let user = try await viewModel.clientApp.getUser() else {
+ viewModel.logCall("getTeam()", result: "Error: No user signed in")
+ return
+ }
+ let team = try await user.getTeam(id: selectedTeamId)
+ if let team = team {
+ let dict = await serializeTeam(team)
+ viewModel.logCall(
+ "user.getTeam(id:)",
+ params: params,
+ result: formatObject("Team", dict)
+ )
+ } else {
+ viewModel.logCall("user.getTeam(id:)", params: params, result: "nil (team not found or not a member)")
+ }
+ } catch {
+ viewModel.logCall("user.getTeam(id:)", params: params, error: error)
+ }
+ }
+
+ func listTeamMembers() async {
+ let params = "teamId: \"\(selectedTeamId)\""
+ viewModel.logInfo("team.listUsers()", message: "Calling...", details: params)
+
+ do {
+ guard let user = try await viewModel.clientApp.getUser() else {
+ viewModel.logCall("team.listUsers()", result: "Error: No user signed in")
+ return
+ }
+ guard let team = try await user.getTeam(id: selectedTeamId) else {
+ viewModel.logCall("team.listUsers()", params: params, result: "Error: Team not found")
+ return
+ }
+ let members = try await team.listUsers()
+ let dicts = members.map { serializeTeamUser($0) }
+ viewModel.logCall("team.listUsers()", params: params, result: formatObjectArray("TeamUser", dicts))
+ } catch {
+ viewModel.logCall("team.listUsers()", params: params, error: error)
+ }
+ }
+}
+
+// MARK: - Contact Channels View
+
+struct ContactChannelsView: View {
+ @Bindable var viewModel: SDKTestViewModel
+ @State private var channels: [(id: String, value: String, isPrimary: Bool, isVerified: Bool)] = []
+
+ var body: some View {
+ Form {
Section("Contact Channels") {
- Button("List Contact Channels") {
+ Button("user.listContactChannels()") {
Task { await listChannels() }
}
@@ -429,322 +1206,318 @@ struct UserView: View {
.font(.caption)
.foregroundStyle(.blue)
}
+ if channel.isVerified {
+ Text("Verified")
+ .font(.caption)
+ .foregroundStyle(.green)
+ }
}
}
}
}
- .navigationTitle("User")
+ .navigationTitle("Contact Channels")
}
- func setDisplayName() async {
+ func listChannels() async {
+ viewModel.logInfo("listContactChannels()", message: "Calling...")
+
do {
guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
+ viewModel.logCall("listContactChannels()", result: "Error: No user signed in")
return
}
- viewModel.log("Setting display name: \(displayName)")
- try await user.setDisplayName(displayName)
- viewModel.log("Display name set!", type: .success)
+ let channelsList = try await user.listContactChannels()
+ var results: [(id: String, value: String, isPrimary: Bool, isVerified: Bool)] = []
+ var dicts: [[String: Any]] = []
+ for channel in channelsList {
+ let dict = await serializeContactChannel(channel)
+ dicts.append(dict)
+ results.append((
+ id: channel.id,
+ value: dict["value"] as? String ?? "",
+ isPrimary: dict["isPrimary"] as? Bool ?? false,
+ isVerified: dict["isVerified"] as? Bool ?? false
+ ))
+ }
+ channels = results
+ viewModel.logCall("user.listContactChannels()", result: formatObjectArray("ContactChannel", dicts))
} catch {
- viewModel.log("Set display name failed: \(error)", type: .error)
+ viewModel.logCall("user.listContactChannels()", error: error)
}
}
+}
+
+// MARK: - OAuth View
+
+struct OAuthView: View {
+ @Bindable var viewModel: SDKTestViewModel
+ @State private var provider = "google"
+ @State private var isSigningIn = false
+ private let presentationProvider = iOSPresentationContextProvider()
- func updateMetadata() async {
- do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
+ var body: some View {
+ Form {
+ Section("Sign In with OAuth") {
+ TextField("Provider", text: $provider)
+ .textInputAutocapitalization(.never)
+
+ HStack {
+ Button("google") { provider = "google" }
+ Button("github") { provider = "github" }
+ Button("microsoft") { provider = "microsoft" }
+ }
+ .buttonStyle(.bordered)
+
+ Button {
+ Task { await signInWithOAuth() }
+ } label: {
+ HStack {
+ if isSigningIn {
+ ProgressView()
+ .scaleEffect(0.8)
+ }
+ Text("signInWithOAuth(provider: \"\(provider)\")")
+ }
+ }
+ .disabled(isSigningIn)
+ }
+
+ Section("OAuth URL Generation (Manual)") {
+ Button("getOAuthUrl(provider: \"\(provider)\")") {
+ Task { await getOAuthUrl() }
+ }
+
+ Text("Returns URL, state, and codeVerifier for manual OAuth handling")
+ .font(.caption)
+ .foregroundStyle(.secondary)
}
- viewModel.log("Updating metadata: \(metadataKey)=\(metadataValue)")
- try await user.update(clientMetadata: [metadataKey: metadataValue])
- viewModel.log("Metadata updated!", type: .success)
- } catch {
- viewModel.log("Update metadata failed: \(error)", type: .error)
}
+ .navigationTitle("OAuth")
}
- func updatePassword() async {
+ func signInWithOAuth() async {
+ let params = "provider: \"\(provider)\""
+ viewModel.logInfo("signInWithOAuth()", message: "Opening OAuth browser...", details: params)
+ isSigningIn = true
+
do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
+ try await viewModel.clientApp.signInWithOAuth(
+ provider: provider,
+ presentationContextProvider: presentationProvider
+ )
+ viewModel.logCall(
+ "signInWithOAuth(provider:)",
+ params: params,
+ result: "Success! User signed in via OAuth."
+ )
+ // Fetch user to show details
+ if let user = try await viewModel.clientApp.getUser() {
+ let dict = await serializeCurrentUser(user)
+ viewModel.logCall(
+ "getUser() after OAuth",
+ result: formatObject("CurrentUser", dict)
+ )
}
- viewModel.log("Updating password...")
- try await user.updatePassword(oldPassword: oldPassword, newPassword: newPassword)
- viewModel.log("Password updated!", type: .success)
} catch {
- viewModel.log("Update password failed: \(error)", type: .error)
+ viewModel.logCall("signInWithOAuth(provider:)", params: params, error: error)
}
+
+ isSigningIn = false
}
- func updatePasswordWrong() async {
+ func getOAuthUrl() async {
+ let params = "provider: \"\(provider)\""
+ viewModel.logInfo("getOAuthUrl()", message: "Calling...", details: params)
+
do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
- }
- viewModel.log("Updating password with wrong old...")
- try await user.updatePassword(oldPassword: "WrongPassword!", newPassword: newPassword)
- viewModel.log("Password updated (unexpected)", type: .error)
- } catch let error as PasswordConfirmationMismatchError {
- viewModel.log("Got PasswordConfirmationMismatchError", type: .success)
+ let result = try await viewModel.clientApp.getOAuthUrl(provider: provider)
+ viewModel.logCall(
+ "getOAuthUrl(provider:)",
+ params: params,
+ result: "OAuthUrlResult {\n url: \"\(result.url)\"\n state: \"\(result.state)\"\n codeVerifier: \"\(result.codeVerifier)\"\n}"
+ )
} catch {
- viewModel.log("Unexpected error: \(error)", type: .error)
+ viewModel.logCall("getOAuthUrl(provider:)", params: params, error: error)
}
}
+}
+
+// MARK: - Tokens View
+
+struct TokensView: View {
+ @Bindable var viewModel: SDKTestViewModel
+
+ var body: some View {
+ Form {
+ Section("Token Operations") {
+ Button("getAccessToken()") {
+ Task { await getAccessToken() }
+ }
+
+ Button("getRefreshToken()") {
+ Task { await getRefreshToken() }
+ }
+
+ Button("getAuthHeaders()") {
+ Task { await getAuthHeaders() }
+ }
+ }
+
+ Section("Token Store Types") {
+ Button("Test Memory Store") {
+ Task { await testMemoryStore() }
+ }
+
+ Button("Test Explicit Store") {
+ Task { await testExplicitStore() }
+ }
+ }
+ }
+ .navigationTitle("Tokens")
+ }
func getAccessToken() async {
+ viewModel.logInfo("getAccessToken()", message: "Calling...")
+
let token = await viewModel.clientApp.getAccessToken()
if let token = token {
- viewModel.log("Access token: \(token.prefix(40))...", type: .success)
+ let parts = token.split(separator: ".")
+ viewModel.logCall(
+ "getAccessToken()",
+ result: "JWT Token:\n Parts: \(parts.count)\n Length: \(token.count) chars\n Token: \(token)"
+ )
} else {
- viewModel.log("No access token", type: .info)
+ viewModel.logCall("getAccessToken()", result: "nil")
}
}
func getRefreshToken() async {
+ viewModel.logInfo("getRefreshToken()", message: "Calling...")
+
let token = await viewModel.clientApp.getRefreshToken()
if let token = token {
- viewModel.log("Refresh token: \(token.prefix(20))...", type: .success)
+ viewModel.logCall(
+ "getRefreshToken()",
+ result: "Refresh Token:\n Length: \(token.count) chars\n Token: \(token)"
+ )
} else {
- viewModel.log("No refresh token", type: .info)
+ viewModel.logCall("getRefreshToken()", result: "nil")
}
}
func getAuthHeaders() async {
+ viewModel.logInfo("getAuthHeaders()", message: "Calling...")
+
let headers = await viewModel.clientApp.getAuthHeaders()
- viewModel.log("Auth headers: \(headers.keys.joined(separator: ", "))", type: .success)
- }
-
- func getPartialUser() async {
- let user = await viewModel.clientApp.getPartialUser()
- if let user = user {
- viewModel.log("Partial user: \(user.primaryEmail ?? "nil")", type: .success)
- } else {
- viewModel.log("No partial user", type: .info)
+ var result = "Headers {\n"
+ for (key, value) in headers {
+ result += " \"\(key)\": \"\(value)\"\n"
}
+ result += "}"
+ viewModel.logCall("getAuthHeaders()", result: result)
}
- func listChannels() async {
+ func testMemoryStore() async {
+ viewModel.logInfo("StackClientApp(tokenStore: .memory)", message: "Creating app with memory store...")
+
+ let app = StackClientApp(
+ projectId: viewModel.projectId,
+ publishableClientKey: viewModel.publishableClientKey,
+ baseUrl: viewModel.baseUrl,
+ tokenStore: .memory,
+ noAutomaticPrefetch: true
+ )
+ let token = await app.getAccessToken()
+ viewModel.logCall(
+ "StackClientApp(tokenStore: .memory)",
+ result: "Created app with memory store\ngetAccessToken() = \(token == nil ? "nil" : "present")"
+ )
+ }
+
+ func testExplicitStore() async {
+ viewModel.logInfo("Testing explicit token store...", message: "Getting tokens from current app...")
+
+ let accessToken = await viewModel.clientApp.getAccessToken()
+ let refreshToken = await viewModel.clientApp.getRefreshToken()
+
+ guard let at = accessToken, let rt = refreshToken else {
+ viewModel.logCall("testExplicitStore()", result: "Error: No tokens available. Sign in first.")
+ return
+ }
+
+ let app = StackClientApp(
+ projectId: viewModel.projectId,
+ publishableClientKey: viewModel.publishableClientKey,
+ baseUrl: viewModel.baseUrl,
+ tokenStore: .explicit(accessToken: at, refreshToken: rt),
+ noAutomaticPrefetch: true
+ )
+
do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
+ let user = try await app.getUser()
+ if let user = user {
+ let email = await user.primaryEmail
+ viewModel.logCall(
+ "StackClientApp(tokenStore: .explicit(...))",
+ result: "Success! Created app with explicit tokens\ngetUser() returned: \(email ?? "no email")"
+ )
+ } else {
+ viewModel.logCall(
+ "StackClientApp(tokenStore: .explicit(...))",
+ result: "App created but getUser() returned nil"
+ )
}
- viewModel.log("Listing contact channels...")
- let channelsList = try await user.listContactChannels()
- var results: [(id: String, value: String, isPrimary: Bool)] = []
- for channel in channelsList {
- let value = await channel.value
- let isPrimary = await channel.isPrimary
- results.append((id: channel.id, value: value, isPrimary: isPrimary))
- }
- channels = results
- viewModel.log("Found \(channels.count) channels", type: .success)
} catch {
- viewModel.log("List channels failed: \(error)", type: .error)
+ viewModel.logCall("StackClientApp(tokenStore: .explicit(...))", error: error)
}
}
}
-// MARK: - Teams View
+// MARK: - Server Users View
-struct TeamsView: View {
- @Bindable var viewModel: SDKTestViewModel
- @State private var teamName = ""
- @State private var teams: [(id: String, name: String)] = []
- @State private var selectedTeamId = ""
- @State private var teamMembers: [String] = []
-
- var body: some View {
- List {
- Section("Create Team") {
- TextField("Team Name", text: $teamName)
-
- Button("Generate Random Name") {
- teamName = "Team \(UUID().uuidString.prefix(8))"
- }
-
- Button("Create Team") {
- Task { await createTeam() }
- }
- .disabled(teamName.isEmpty)
- }
-
- Section("My Teams") {
- Button("Refresh Teams") {
- Task { await listTeams() }
- }
-
- ForEach(teams, id: \.id) { team in
- Button {
- selectedTeamId = team.id
- Task { await listTeamMembers() }
- } label: {
- HStack {
- Text(team.name)
- Spacer()
- if team.id == selectedTeamId {
- Image(systemName: "checkmark")
- }
- }
- }
- }
- }
-
- if !selectedTeamId.isEmpty {
- Section("Team Members (\(selectedTeamId.prefix(8))...)") {
- Button("Refresh Members") {
- Task { await listTeamMembers() }
- }
-
- ForEach(teamMembers, id: \.self) { userId in
- Text(userId)
- .font(.caption)
- }
- }
-
- Section("Team Actions") {
- Button("Update Team Name") {
- Task { await updateTeamName() }
- }
- .disabled(teamName.isEmpty)
- }
- }
- }
- .navigationTitle("Teams")
- .onAppear {
- Task { await listTeams() }
- }
- }
-
- func createTeam() async {
- do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
- }
- viewModel.log("Creating team: \(teamName)")
- let team = try await user.createTeam(displayName: teamName)
- viewModel.log("Team created: \(team.id)", type: .success)
- await listTeams()
- } catch {
- viewModel.log("Create team failed: \(error)", type: .error)
- }
- }
-
- func listTeams() async {
- do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
- }
- viewModel.log("Listing teams...")
- let teamsList = try await user.listTeams()
- var results: [(id: String, name: String)] = []
- for team in teamsList {
- let name = await team.displayName
- results.append((id: team.id, name: name))
- }
- teams = results
- viewModel.log("Found \(teams.count) teams", type: .success)
- } catch {
- viewModel.log("List teams failed: \(error)", type: .error)
- }
- }
-
- func listTeamMembers() async {
- do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
- }
- guard let team = try await user.getTeam(id: selectedTeamId) else {
- viewModel.log("Team not found", type: .error)
- return
- }
- viewModel.log("Listing team members...")
- let members = try await team.listUsers()
- teamMembers = members.map { $0.id }
- viewModel.log("Found \(members.count) members", type: .success)
- } catch {
- viewModel.log("List members failed: \(error)", type: .error)
- }
- }
-
- func updateTeamName() async {
- do {
- guard let user = try await viewModel.clientApp.getUser() else {
- viewModel.log("No user signed in", type: .error)
- return
- }
- guard let team = try await user.getTeam(id: selectedTeamId) else {
- viewModel.log("Team not found", type: .error)
- return
- }
- viewModel.log("Updating team name: \(teamName)")
- try await team.update(displayName: teamName)
- viewModel.log("Team updated!", type: .success)
- await listTeams()
- } catch {
- viewModel.log("Update team failed: \(error)", type: .error)
- }
- }
-}
-
-// MARK: - Server View
-
-struct ServerView: View {
+struct ServerUsersView: View {
@Bindable var viewModel: SDKTestViewModel
@State private var email = ""
@State private var displayName = ""
@State private var userId = ""
- @State private var teamName = ""
- @State private var teamId = ""
@State private var users: [(id: String, email: String?)] = []
- @State private var teams: [(id: String, name: String)] = []
var body: some View {
- List {
+ Form {
Section("Create User") {
TextField("Email", text: $email)
.textInputAutocapitalization(.never)
- .autocorrectionDisabled()
.keyboardType(.emailAddress)
- TextField("Display Name", text: $displayName)
+ TextField("Display Name (optional)", text: $displayName)
Button("Generate Random Email") {
- email = "test-\(UUID().uuidString.lowercased().prefix(8))@example.com"
+ email = "test-\(UUID().uuidString.lowercased())@example.com"
+ viewModel.logInfo("generateEmail()", message: "Generated: \(email)")
}
- Button("Create User") {
+ Button("serverApp.createUser(email: email)") {
Task { await createUser() }
}
.disabled(email.isEmpty)
-
- Button("Create User (All Options)") {
- Task { await createUserWithOptions() }
- }
- .disabled(email.isEmpty)
}
- Section("Users") {
- Button("List Users") {
+ Section("List Users") {
+ Button("serverApp.listUsers(limit: 5)") {
Task { await listUsers() }
}
ForEach(users, id: \.id) { user in
- Button {
- userId = user.id
- } label: {
- HStack {
- Text(user.email ?? "no email")
- Spacer()
- if user.id == userId {
- Image(systemName: "checkmark")
- }
+ HStack {
+ Text(user.email ?? "no email")
+ Spacer()
+ Text(user.id.prefix(8) + "...")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Button("Select") {
+ userId = user.id
+ viewModel.logInfo("selectUser()", message: "Selected: \(user.id)")
}
+ .buttonStyle(.borderless)
}
}
}
@@ -752,261 +1525,343 @@ struct ServerView: View {
Section("User Operations") {
TextField("User ID", text: $userId)
.textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- Button("Get User") {
+ Button("serverApp.getUser(id: userId)") {
Task { await getUser() }
}
.disabled(userId.isEmpty)
- Button("Delete User") {
+ Button("user.delete()") {
Task { await deleteUser() }
}
.disabled(userId.isEmpty)
-
- Button("Create Session (Impersonate)") {
- Task { await createSession() }
- }
- .disabled(userId.isEmpty)
}
-
+ }
+ .navigationTitle("Server Users")
+ }
+
+ func createUser() async {
+ let params = "email: \"\(email)\""
+ viewModel.logInfo("createUser()", message: "Calling...", details: params)
+
+ do {
+ let user = try await viewModel.serverApp.createUser(email: email)
+ let dict = await serializeServerUser(user)
+ viewModel.logCall(
+ "serverApp.createUser(email:)",
+ params: params,
+ result: formatObject("ServerUser", dict)
+ )
+ userId = user.id
+ await listUsers()
+ } catch {
+ viewModel.logCall("serverApp.createUser(email:)", params: params, error: error)
+ }
+ }
+
+ func listUsers() async {
+ let params = "limit: 5"
+ viewModel.logInfo("listUsers()", message: "Calling...", details: params)
+
+ do {
+ let result = try await viewModel.serverApp.listUsers(limit: 5)
+ var usersList: [(id: String, email: String?)] = []
+ var dicts: [[String: Any]] = []
+ for user in result.items {
+ let dict = await serializeServerUser(user)
+ dicts.append(dict)
+ usersList.append((id: user.id, email: dict["primaryEmail"] as? String))
+ }
+ users = usersList
+ viewModel.logCall("serverApp.listUsers(limit:)", params: params, result: formatObjectArray("ServerUser", dicts))
+ } catch {
+ viewModel.logCall("serverApp.listUsers(limit:)", params: params, error: error)
+ }
+ }
+
+ func getUser() async {
+ let params = "id: \"\(userId)\""
+ viewModel.logInfo("getUser()", message: "Calling...", details: params)
+
+ do {
+ let user = try await viewModel.serverApp.getUser(id: userId)
+ if let user = user {
+ let dict = await serializeServerUser(user)
+ viewModel.logCall(
+ "serverApp.getUser(id:)",
+ params: params,
+ result: formatObject("ServerUser", dict)
+ )
+ } else {
+ viewModel.logCall("serverApp.getUser(id:)", params: params, result: "nil (user not found)")
+ }
+ } catch {
+ viewModel.logCall("serverApp.getUser(id:)", params: params, error: error)
+ }
+ }
+
+ func deleteUser() async {
+ let params = "userId: \"\(userId)\""
+ viewModel.logInfo("user.delete()", message: "Calling...", details: params)
+
+ do {
+ guard let user = try await viewModel.serverApp.getUser(id: userId) else {
+ viewModel.logCall("user.delete()", params: params, result: "Error: User not found")
+ return
+ }
+ try await user.delete()
+ viewModel.logCall("user.delete()", params: params, result: "Success! User deleted.")
+ userId = ""
+ await listUsers()
+ } catch {
+ viewModel.logCall("user.delete()", params: params, error: error)
+ }
+ }
+}
+
+// MARK: - Server Teams View
+
+struct ServerTeamsView: View {
+ @Bindable var viewModel: SDKTestViewModel
+ @State private var teamName = ""
+ @State private var teamId = ""
+ @State private var userIdToAdd = ""
+ @State private var teams: [(id: String, name: String)] = []
+
+ var body: some View {
+ Form {
Section("Create Team") {
TextField("Team Name", text: $teamName)
Button("Generate Random Name") {
teamName = "Team \(UUID().uuidString.prefix(8))"
+ viewModel.logInfo("generateTeamName()", message: "Generated: \(teamName)")
}
- Button("Create Team") {
+ Button("serverApp.createTeam(displayName: teamName)") {
Task { await createTeam() }
}
.disabled(teamName.isEmpty)
}
- Section("Teams") {
- Button("List Teams") {
+ Section("List Teams") {
+ Button("serverApp.listTeams()") {
Task { await listTeams() }
}
ForEach(teams, id: \.id) { team in
- Button {
- teamId = team.id
- } label: {
- HStack {
- Text(team.name)
- Spacer()
- if team.id == teamId {
- Image(systemName: "checkmark")
- }
+ HStack {
+ Text(team.name)
+ Spacer()
+ Text(team.id.prefix(8) + "...")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Button("Select") {
+ teamId = team.id
+ viewModel.logInfo("selectTeam()", message: "Selected: \(team.id)")
}
+ .buttonStyle(.borderless)
}
}
}
- Section("Team Operations") {
+ Section("Team Membership") {
TextField("Team ID", text: $teamId)
.textInputAutocapitalization(.never)
- .autocorrectionDisabled()
+ TextField("User ID", text: $userIdToAdd)
+ .textInputAutocapitalization(.never)
- Button("Add User to Team") {
+ Button("team.addUser(id: userId)") {
Task { await addUserToTeam() }
}
- .disabled(teamId.isEmpty || userId.isEmpty)
+ .disabled(teamId.isEmpty || userIdToAdd.isEmpty)
- Button("Remove User from Team") {
+ Button("team.removeUser(id: userId)") {
Task { await removeUserFromTeam() }
}
- .disabled(teamId.isEmpty || userId.isEmpty)
-
- Button("List Team Users") {
- Task { await listTeamUsers() }
- }
- .disabled(teamId.isEmpty)
-
- Button("Delete Team") {
- Task { await deleteTeam() }
- }
- .disabled(teamId.isEmpty)
+ .disabled(teamId.isEmpty || userIdToAdd.isEmpty)
}
}
- .navigationTitle("Server")
- }
-
- func createUser() async {
- do {
- viewModel.log("Creating user: \(email)")
- let user = try await viewModel.serverApp.createUser(email: email)
- viewModel.log("User created: \(user.id)", type: .success)
- userId = user.id
- await listUsers()
- } catch {
- viewModel.log("Create user failed: \(error)", type: .error)
- }
- }
-
- func createUserWithOptions() async {
- do {
- viewModel.log("Creating user with options: \(email)")
- let user = try await viewModel.serverApp.createUser(
- email: email,
- password: "TestPassword123!",
- displayName: displayName.isEmpty ? nil : displayName,
- primaryEmailVerified: true,
- clientMetadata: ["source": "iOS-example"],
- serverMetadata: ["created_via": "example-app"]
- )
- viewModel.log("User created: \(user.id)", type: .success)
- userId = user.id
- await listUsers()
- } catch {
- viewModel.log("Create user failed: \(error)", type: .error)
- }
- }
-
- func listUsers() async {
- do {
- viewModel.log("Listing users...")
- let result = try await viewModel.serverApp.listUsers(limit: 5)
- var usersList: [(id: String, email: String?)] = []
- for user in result.items {
- let email = await user.primaryEmail
- usersList.append((id: user.id, email: email))
- }
- users = usersList
- viewModel.log("Found \(users.count) users", type: .success)
- } catch {
- viewModel.log("List users failed: \(error)", type: .error)
- }
- }
-
- func getUser() async {
- do {
- viewModel.log("Getting user: \(userId)")
- let user = try await viewModel.serverApp.getUser(id: userId)
- if let user = user {
- let email = await user.primaryEmail
- viewModel.log("User: \(email ?? "nil")", type: .success)
- } else {
- viewModel.log("User not found", type: .info)
- }
- } catch {
- viewModel.log("Get user failed: \(error)", type: .error)
- }
- }
-
- func deleteUser() async {
- do {
- viewModel.log("Deleting user: \(userId)")
- guard let user = try await viewModel.serverApp.getUser(id: userId) else {
- viewModel.log("User not found", type: .error)
- return
- }
- try await user.delete()
- viewModel.log("User deleted!", type: .success)
- userId = ""
- await listUsers()
- } catch {
- viewModel.log("Delete user failed: \(error)", type: .error)
- }
- }
-
- func createSession() async {
- do {
- viewModel.log("Creating session for: \(userId)")
- let tokens = try await viewModel.serverApp.createSession(userId: userId)
- viewModel.log("Session created!", type: .success)
- viewModel.log("Access token: \(tokens.accessToken.prefix(30))...", type: .info)
- } catch {
- viewModel.log("Create session failed: \(error)", type: .error)
- }
+ .navigationTitle("Server Teams")
}
func createTeam() async {
+ let params = "displayName: \"\(teamName)\""
+ viewModel.logInfo("createTeam()", message: "Calling...", details: params)
+
do {
- viewModel.log("Creating team: \(teamName)")
let team = try await viewModel.serverApp.createTeam(displayName: teamName)
- viewModel.log("Team created: \(team.id)", type: .success)
+ let dict = await serializeServerTeam(team)
+ viewModel.logCall(
+ "serverApp.createTeam(displayName:)",
+ params: params,
+ result: formatObject("ServerTeam", dict)
+ )
teamId = team.id
await listTeams()
} catch {
- viewModel.log("Create team failed: \(error)", type: .error)
+ viewModel.logCall("serverApp.createTeam(displayName:)", params: params, error: error)
}
}
func listTeams() async {
+ viewModel.logInfo("listTeams()", message: "Calling...")
+
do {
- viewModel.log("Listing teams...")
let teamsList = try await viewModel.serverApp.listTeams()
var results: [(id: String, name: String)] = []
+ var dicts: [[String: Any]] = []
for team in teamsList {
- let name = await team.displayName
- results.append((id: team.id, name: name))
+ let dict = await serializeServerTeam(team)
+ dicts.append(dict)
+ results.append((id: team.id, name: dict["displayName"] as? String ?? ""))
}
teams = results
- viewModel.log("Found \(teams.count) teams", type: .success)
+ viewModel.logCall("serverApp.listTeams()", result: formatObjectArray("ServerTeam", dicts))
} catch {
- viewModel.log("List teams failed: \(error)", type: .error)
+ viewModel.logCall("serverApp.listTeams()", error: error)
}
}
func addUserToTeam() async {
+ let params = "teamId: \"\(teamId)\"\nuserId: \"\(userIdToAdd)\""
+ viewModel.logInfo("team.addUser()", message: "Calling...", details: params)
+
do {
- viewModel.log("Adding user to team...")
guard let team = try await viewModel.serverApp.getTeam(id: teamId) else {
- viewModel.log("Team not found", type: .error)
+ viewModel.logCall("team.addUser()", params: params, result: "Error: Team not found")
return
}
- try await team.addUser(id: userId)
- viewModel.log("User added to team!", type: .success)
+ try await team.addUser(id: userIdToAdd)
+ let dict = await serializeServerTeam(team)
+ viewModel.logCall("team.addUser(id:)", params: params, result: "Success! User added to team.\n\n" + formatObject("ServerTeam", dict))
} catch {
- viewModel.log("Add user failed: \(error)", type: .error)
+ viewModel.logCall("team.addUser(id:)", params: params, error: error)
}
}
func removeUserFromTeam() async {
+ let params = "teamId: \"\(teamId)\"\nuserId: \"\(userIdToAdd)\""
+ viewModel.logInfo("team.removeUser()", message: "Calling...", details: params)
+
do {
- viewModel.log("Removing user from team...")
guard let team = try await viewModel.serverApp.getTeam(id: teamId) else {
- viewModel.log("Team not found", type: .error)
+ viewModel.logCall("team.removeUser()", params: params, result: "Error: Team not found")
return
}
- try await team.removeUser(id: userId)
- viewModel.log("User removed from team!", type: .success)
+ try await team.removeUser(id: userIdToAdd)
+ let dict = await serializeServerTeam(team)
+ viewModel.logCall("team.removeUser(id:)", params: params, result: "Success! User removed from team.\n\n" + formatObject("ServerTeam", dict))
} catch {
- viewModel.log("Remove user failed: \(error)", type: .error)
+ viewModel.logCall("team.removeUser(id:)", params: params, error: error)
+ }
+ }
+}
+
+// MARK: - Sessions View
+
+struct SessionsView: View {
+ @Bindable var viewModel: SDKTestViewModel
+ @State private var userId = ""
+ @State private var accessToken = ""
+ @State private var refreshToken = ""
+
+ var body: some View {
+ Form {
+ Section("Create Session (Impersonation)") {
+ TextField("User ID", text: $userId)
+ .textInputAutocapitalization(.never)
+
+ Button("serverApp.createSession(userId: userId)") {
+ Task { await createSession() }
+ }
+ .disabled(userId.isEmpty)
+ }
+
+ if !accessToken.isEmpty {
+ Section("Session Tokens") {
+ VStack(alignment: .leading) {
+ Text("Access Token:")
+ .font(.headline)
+ Text(accessToken.prefix(100) + "...")
+ .font(.system(.caption, design: .monospaced))
+ }
+
+ VStack(alignment: .leading) {
+ Text("Refresh Token:")
+ .font(.headline)
+ Text(refreshToken.prefix(50) + "...")
+ .font(.system(.caption, design: .monospaced))
+ }
+
+ Button("Copy Access Token") {
+ UIPasteboard.general.string = accessToken
+ }
+
+ Button("Copy Refresh Token") {
+ UIPasteboard.general.string = refreshToken
+ }
+ }
+
+ Section("Use Session") {
+ Button("Create Client with Session Tokens") {
+ Task { await useSessionTokens() }
+ }
+ }
+ }
+ }
+ .navigationTitle("Sessions")
+ }
+
+ func createSession() async {
+ let params = "userId: \"\(userId)\""
+ viewModel.logInfo("createSession()", message: "Calling...", details: params)
+
+ do {
+ let tokens = try await viewModel.serverApp.createSession(userId: userId)
+ accessToken = tokens.accessToken
+ refreshToken = tokens.refreshToken
+ viewModel.logCall(
+ "serverApp.createSession(userId:)",
+ params: params,
+ result: """
+ SessionTokens {
+ accessToken: "\(tokens.accessToken.prefix(50))..."
+ refreshToken: "\(tokens.refreshToken.prefix(30))..."
+ }
+ """
+ )
+ } catch {
+ viewModel.logCall("serverApp.createSession(userId:)", params: params, error: error)
}
}
- func listTeamUsers() async {
+ func useSessionTokens() async {
+ viewModel.logInfo("StackClientApp(tokenStore: .explicit(...))", message: "Creating client with session tokens...")
+
do {
- viewModel.log("Listing team users...")
- guard let team = try await viewModel.serverApp.getTeam(id: teamId) else {
- viewModel.log("Team not found", type: .error)
- return
- }
- let users = try await team.listUsers()
- viewModel.log("Found \(users.count) users", type: .success)
- for user in users {
- viewModel.log(" - \(user.id)", type: .info)
+ let client = StackClientApp(
+ projectId: viewModel.projectId,
+ publishableClientKey: viewModel.publishableClientKey,
+ baseUrl: viewModel.baseUrl,
+ tokenStore: .explicit(accessToken: accessToken, refreshToken: refreshToken),
+ noAutomaticPrefetch: true
+ )
+ let user = try await client.getUser()
+ if let user = user {
+ let dict = await serializeCurrentUser(user)
+ viewModel.logCall(
+ "clientWithTokens.getUser()",
+ result: "Success! Authenticated user:\n\n" + formatObject("CurrentUser", dict)
+ )
+ } else {
+ viewModel.logCall(
+ "clientWithTokens.getUser()",
+ result: "nil (tokens may be invalid)"
+ )
}
} catch {
- viewModel.log("List team users failed: \(error)", type: .error)
- }
- }
-
- func deleteTeam() async {
- do {
- viewModel.log("Deleting team: \(teamId)")
- guard let team = try await viewModel.serverApp.getTeam(id: teamId) else {
- viewModel.log("Team not found", type: .error)
- return
- }
- try await team.delete()
- viewModel.log("Team deleted!", type: .success)
- teamId = ""
- await listTeams()
- } catch {
- viewModel.log("Delete team failed: \(error)", type: .error)
+ viewModel.logCall("clientWithTokens.getUser()", error: error)
}
}
}
diff --git a/sdks/implementations/swift/package.json b/sdks/implementations/swift/package.json
index e0f6f4051..d199e059d 100644
--- a/sdks/implementations/swift/package.json
+++ b/sdks/implementations/swift/package.json
@@ -7,6 +7,6 @@
"test": "swift test",
"build": "swift build",
"start:mac-example": "cd Examples/StackAuthMacOS && swift run",
- "start:ios-example": "echo 'iOS example requires Xcode. Run: open Examples/StackAuthiOS/Package.swift'"
+ "start:ios-example": "echo 'iOS example requires Xcode. Run: open Examples/StackAuthiOS/StackAuthiOS.xcodeproj'"
}
}