Skip to content
Merged
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
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,67 @@ jobs:
- name: Build & Test macOS
run: bash scripts/build-macos.sh

test-app:
name: Test App (macOS)
runs-on: self-hosted
steps:
- uses: actions/checkout@v6
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Write .env file
env:
ENV_B64: ${{ secrets.ENV_B64 }}
run: echo "$ENV_B64" | base64 --decode > test/testUITests/.env
- name: Run TestPlan
run: |
xcodebuild test \
-project test/test.xcodeproj \
-scheme test \
-testPlan TestPlan \
-destination 'platform=macOS' \
CODE_SIGN_IDENTITY=- \
DEVELOPMENT_TEAM=""

test-app-ios:
name: Test App (iOS)
runs-on: self-hosted
steps:
- uses: actions/checkout@v6
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Write .env file
env:
ENV_B64: ${{ secrets.ENV_B64 }}
run: echo "$ENV_B64" | base64 --decode > test/testUITests/.env
- name: Find iOS Simulator
id: sim
run: |
xcrun simctl list runtimes
xcrun simctl list devices available
SIM_ID=$(xcrun simctl list devices available -j | python3 -c "
import json, sys
data = json.load(sys.stdin)
for runtime, devices in data['devices'].items():
if 'iOS' in runtime:
for d in devices:
if d['isAvailable']:
print(d['udid'])
sys.exit(0)
sys.exit(1)
")
echo "sim_id=$SIM_ID" >> "$GITHUB_OUTPUT"
- name: Run TestPlan
run: |
xcodebuild test \
-project test/test.xcodeproj \
-scheme test \
-testPlan TestPlan \
-destination 'platform=iOS Simulator,id=${{ steps.sim.outputs.sim_id }}' \
CODE_SIGN_IDENTITY=- \
DEVELOPMENT_TEAM=""

build-ios:
name: Build (iOS)
runs-on: macos-latest
Expand Down
22 changes: 20 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
.DS_Store
/.build
/Packages
.netrc
.env

# Xcode
xcuserdata/
DerivedData/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
*.moved-aside
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

# Swift Package Manager
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.env
.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
73 changes: 73 additions & 0 deletions Sources/RxAuthSwift/InMemoryTokenStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Foundation

public final class InMemoryTokenStorage: TokenStorageProtocol, @unchecked Sendable {

Check warning on line 3 in Sources/RxAuthSwift/InMemoryTokenStorage.swift

View check run for this annotation

Autopilot Project manager / Autopilot PR Check

Mock Implementation

InMemoryTokenStorage appears to be a test double (Fake) implementation using in-memory variables instead of persistent storage. This is typically intended for testing, not production.
private let lock = NSLock()
private var accessToken: String?
private var refreshToken: String?
private var expiresAt: Date?

public init() {}

public func saveAccessToken(_ token: String) throws {
lock.lock()
defer { lock.unlock() }
accessToken = token
}

public func getAccessToken() -> String? {
lock.lock()
defer { lock.unlock() }
return accessToken
}

public func deleteAccessToken() throws {
lock.lock()
defer { lock.unlock() }
accessToken = nil
}

public func saveRefreshToken(_ token: String) throws {
lock.lock()
defer { lock.unlock() }
refreshToken = token
}

public func getRefreshToken() -> String? {
lock.lock()
defer { lock.unlock() }
return refreshToken
}

public func deleteRefreshToken() throws {
lock.lock()
defer { lock.unlock() }
refreshToken = nil
}

public func saveExpiresAt(_ date: Date) throws {
lock.lock()
defer { lock.unlock() }
expiresAt = date
}

public func getExpiresAt() -> Date? {
lock.lock()
defer { lock.unlock() }
return expiresAt
}

public func isTokenExpired() -> Bool {
lock.lock()
defer { lock.unlock() }
guard let expiresAt else { return true }
return expiresAt.timeIntervalSinceNow < 600
}

public func clearAll() throws {
lock.lock()
defer { lock.unlock() }
accessToken = nil
refreshToken = nil
expiresAt = nil
}
}
37 changes: 34 additions & 3 deletions Sources/RxAuthSwift/Platform/MacOSWebAuthSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WebKit
final class MacOSWebAuthSession: NSObject, WebAuthSessionProvider {
private var window: NSWindow?
private var webView: WKWebView?
private var urlTextField: NSTextField?
private var continuation: CheckedContinuation<URL, any Error>?
private var callbackScheme: String = ""

Expand All @@ -25,14 +26,36 @@ final class MacOSWebAuthSession: NSObject, WebAuthSessionProvider {
webView.navigationDelegate = self
self.webView = webView

let urlField = NSTextField()
urlField.isEditable = false
urlField.isSelectable = true
urlField.isBordered = true
urlField.bezelStyle = .roundedBezel
urlField.font = .systemFont(ofSize: 13)
urlField.lineBreakMode = .byTruncatingTail
urlField.cell?.truncatesLastVisibleLine = true
urlField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
urlField.setContentHuggingPriority(.defaultLow, for: .horizontal)
urlField.stringValue = url.absoluteString
self.urlTextField = urlField

let toolbar = NSStackView(views: [urlField])
toolbar.orientation = .horizontal
toolbar.edgeInsets = NSEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
toolbar.setHuggingPriority(.required, for: .horizontal)

let container = NSStackView(views: [toolbar, webView])
container.orientation = .vertical
container.spacing = 0

let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 700),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
window.title = "Sign In"
window.contentView = webView
window.contentView = container
window.center()
window.delegate = self
window.makeKeyAndOrderFront(nil)
Expand All @@ -42,10 +65,12 @@ final class MacOSWebAuthSession: NSObject, WebAuthSessionProvider {
}

private func complete(with result: Result<URL, any Error>) {
window?.close()
window = nil
webView?.navigationDelegate = nil
webView = nil
urlTextField = nil

window?.orderOut(nil)
window = nil

let cont = continuation
continuation = nil
Expand Down Expand Up @@ -77,6 +102,12 @@ extension MacOSWebAuthSession: WKNavigationDelegate {
}
}

nonisolated func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
MainActor.assumeIsolated {
self.urlTextField?.stringValue = webView.url?.absoluteString ?? ""
}
}

nonisolated func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) {
MainActor.assumeIsolated {
self.complete(with: .failure(OAuthError.authenticationFailed(error.localizedDescription)))
Expand Down
2 changes: 1 addition & 1 deletion Sources/RxAuthSwiftUI/Components/PrimaryAuthButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public struct PrimaryAuthButton: View {
.rxGlassProminentButton()
.tint(accentColor)
.disabled(isLoading)
.accessibilityIdentifier("primaryAuthButton")
.accessibilityIdentifier("sign-in-button")
.accessibilityHint(isLoading ? "Authentication in progress" : "Double tap to sign in")
Comment on lines +46 to 47
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the accessibility identifier from primaryAuthButton to sign-in-button on a public UI component. That’s effectively a breaking change for any downstream UI automation relying on the old identifier. Consider keeping the existing identifier (or making it configurable) and updating the UI tests to use it, or document this as a breaking change in release notes.

Copilot uses AI. Check for mistakes.
}
}
2 changes: 1 addition & 1 deletion Sources/RxAuthSwiftUI/RxSignInView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SwiftUI
import RxAuthSwift
import SwiftUI

public struct RxSignInView<Header: View>: View {
@Bindable private var manager: OAuthManager
Expand Down
24 changes: 24 additions & 0 deletions test/TestPlan.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "D90F25C0-41BD-4163-83E8-C1BF58CB5D3F",
"name" : "Configuration 1",
"options" : {

}
}
],
"defaultOptions" : {
"testTimeoutsEnabled" : true
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:test.xcodeproj",
"identifier" : "DF8439562F3C847600461950",
"name" : "testUITests"
}
}
],
"version" : 1
}
Loading