Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: linux build #350

Merged
merged 9 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
18 changes: 18 additions & 0 deletions .github/workflows/auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,21 @@ jobs:
run: sudo xcode-select -s /Applications/Xcode_15.3.app
- name: Run tests
run: PLATFORM="${{ matrix.platform }}" SCHEME=Auth make test-library

test-linux:
name: Test Auth (Linux)
strategy:
fail-fast: false
matrix:
swift-version: ["5.9", "5.10"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-deptch: 0
- uses: swift-actions/setup-swift@v2
with:
swift-version: ${{ matrix.swift-version }}
- run: make dot-env
- name: Run tests
run: swift test --filter AuthTests.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ Supabase client for swift. Mirrors the design of [supabase-js](https://github.co

* Documentation: [https://supabase.com/docs/reference/swift/introduction](https://supabase.com/docs/reference/swift/introduction)

## Supported Platforms

| Platform | Support |
|--------|--------|
| iOS | ✅ |
| macOS | ✅ |
| watchOS | ✅ |
| tvOS | ✅ |
| visionOS | ✅ |
| Linux | ☑️ |
| Windows | ☑️ |

> ✅ Official support
>
> ☑️ Works but not officially supported, and not guaranttee to keep working on future versions of the library.

## Usage

Install the library using the Swift Package Manager.
Expand Down
129 changes: 67 additions & 62 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import _Helpers
import AuthenticationServices
import Foundation

#if canImport(AuthenticationServices)
import AuthenticationServices
#endif

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
Expand Down Expand Up @@ -652,75 +655,77 @@ public final class AuthClient: @unchecked Sendable {
return try await session(from: resultURL)
}

/// Sign-in an existing user via a third-party provider using ``ASWebAuthenticationSession``.
///
/// - Parameters:
/// - provider: The third-party provider.
/// - redirectTo: A URL to send the user to after they are confirmed.
/// - scopes: A space-separated list of scopes granted to the OAuth application.
/// - queryParams: Additional query params.
/// - configure: A configuration closure that you can use to customize the internal
/// ``ASWebAuthenticationSession`` object.
///
/// - Note: This method support the PKCE flow.
/// - Warning: Do not call `start()` on the `ASWebAuthenticationSession` object inside the
/// `configure` closure, as the method implementation calls it already.
@available(watchOS 6.2, tvOS 16.0, *)
@discardableResult
public func signInWithOAuth(
provider: Provider,
redirectTo: URL? = nil,
scopes: String? = nil,
queryParams: [(name: String, value: String?)] = [],
configure: @Sendable (_ session: ASWebAuthenticationSession) -> Void = { _ in }
) async throws -> Session {
try await signInWithOAuth(
provider: provider,
redirectTo: redirectTo,
scopes: scopes,
queryParams: queryParams
) { @MainActor url in
try await withCheckedThrowingContinuation { continuation in
guard let callbackScheme = (configuration.redirectToURL ?? redirectTo)?.scheme else {
continuation.resume(throwing: AuthError.invalidRedirectScheme)
return
}

#if !os(tvOS) && !os(watchOS)
var presentationContextProvider: DefaultPresentationContextProvider?
#endif

let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme
) { url, error in
if let error {
continuation.resume(throwing: error)
} else if let url {
continuation.resume(returning: url)
} else {
continuation.resume(throwing: AuthError.missingURL)
#if canImport(AuthenticationServices)
/// Sign-in an existing user via a third-party provider using ``ASWebAuthenticationSession``.
///
/// - Parameters:
/// - provider: The third-party provider.
/// - redirectTo: A URL to send the user to after they are confirmed.
/// - scopes: A space-separated list of scopes granted to the OAuth application.
/// - queryParams: Additional query params.
/// - configure: A configuration closure that you can use to customize the internal
/// ``ASWebAuthenticationSession`` object.
///
/// - Note: This method support the PKCE flow.
/// - Warning: Do not call `start()` on the `ASWebAuthenticationSession` object inside the
/// `configure` closure, as the method implementation calls it already.
@available(watchOS 6.2, tvOS 16.0, *)
@discardableResult
public func signInWithOAuth(
provider: Provider,
redirectTo: URL? = nil,
scopes: String? = nil,
queryParams: [(name: String, value: String?)] = [],
configure: @Sendable (_ session: ASWebAuthenticationSession) -> Void = { _ in }
) async throws -> Session {
try await signInWithOAuth(
provider: provider,
redirectTo: redirectTo,
scopes: scopes,
queryParams: queryParams
) { @MainActor url in
try await withCheckedThrowingContinuation { continuation in
guard let callbackScheme = (configuration.redirectToURL ?? redirectTo)?.scheme else {
continuation.resume(throwing: AuthError.invalidRedirectScheme)
return
}

#if !os(tvOS) && !os(watchOS)
// Keep a strong reference to presentationContextProvider until the flow completes.
_ = presentationContextProvider
var presentationContextProvider: DefaultPresentationContextProvider?
#endif
}

configure(session)

#if !os(tvOS) && !os(watchOS)
if session.presentationContextProvider == nil {
presentationContextProvider = DefaultPresentationContextProvider()
session.presentationContextProvider = presentationContextProvider
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme
) { url, error in
if let error {
continuation.resume(throwing: error)
} else if let url {
continuation.resume(returning: url)
} else {
continuation.resume(throwing: AuthError.missingURL)
}

#if !os(tvOS) && !os(watchOS)
// Keep a strong reference to presentationContextProvider until the flow completes.
_ = presentationContextProvider
#endif
}
#endif

session.start()
configure(session)

#if !os(tvOS) && !os(watchOS)
if session.presentationContextProvider == nil {
presentationContextProvider = DefaultPresentationContextProvider()
session.presentationContextProvider = presentationContextProvider
}
#endif

session.start()
}
}
}
}
#endif

/// Gets the session data from a OAuth2 callback URL.
@discardableResult
Expand Down Expand Up @@ -1281,7 +1286,7 @@ extension AuthClient {
public static let authChangeSessionInfoKey = "AuthClient.authChangeSession"
}

#if !os(tvOS) && !os(watchOS)
#if canImport(AuthenticationServices) && !os(tvOS) && !os(watchOS)
@MainActor
final class DefaultPresentationContextProvider: NSObject,
ASWebAuthenticationPresentationContextProviding
Expand Down
11 changes: 11 additions & 0 deletions Sources/Realtime/V2/WebSocketClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,14 @@ final class SocketStream: AsyncSequence, Sendable {
try await task.send(message)
}
}

#if os(Linux) || os(Windows)
extension URLSessionWebSocketTask {
func receive(completionHandler: @Sendable @escaping (Result<URLSessionWebSocketTask.Message, any Error>) -> Void) {
Task {
let result = await Result(catching: { try await self.receive() })
completionHandler(result)
}
}
}
#endif
11 changes: 6 additions & 5 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -324,17 +324,18 @@ final class AuthClientTests: XCTestCase {
XCTAssertEqual(emitReceivedEvents.value.map(\.0), [.signedIn])
}

@available(watchOS 6.2, tvOS 16.0, *)
func testSignInWithOAuthWithInvalidRedirecTo() async {
let sut = makeSUT()

do {
try await sut.signInWithOAuth(
provider: .google,
redirectTo: nil
) { _ in
XCTFail("Should not call launchFlow.")
}
redirectTo: nil,
launchFlow: { _ in
XCTFail("Should not call launchFlow.")
return URL(string: "https://supabase.com")!
}
)
} catch let error as AuthError {
XCTAssertEqual(error, .invalidRedirectScheme)
} catch {
Expand Down
71 changes: 42 additions & 29 deletions Tests/AuthTests/RequestsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,36 +146,38 @@ final class RequestsTests: XCTestCase {
}
}

func testSessionFromURL() async throws {
let sut = makeSUT(fetch: { request in
let authorizationHeader = request.allHTTPHeaderFields?["Authorization"]
XCTAssertEqual(authorizationHeader, "bearer accesstoken")
return (json(named: "user"), HTTPURLResponse())
})

let currentDate = Date()

Current.sessionManager = .live
Current.sessionStorage.storeSession = { _ in }
Current.codeVerifierStorage.get = { nil }
Current.currentDate = { currentDate }

let url = URL(
string:
"https://dummy-url.com/callback#access_token=accesstoken&expires_in=60&refresh_token=refreshtoken&token_type=bearer"
)!
#if !os(Linux) && !os(Windows)
func testSessionFromURL() async throws {
let sut = makeSUT(fetch: { request in
let authorizationHeader = request.allHTTPHeaderFields?["Authorization"]
XCTAssertEqual(authorizationHeader, "bearer accesstoken")
return (json(named: "user"), HTTPURLResponse.stub())
})

let currentDate = Date()

Current.sessionManager = .live
Current.sessionStorage.storeSession = { _ in }
Current.codeVerifierStorage.get = { nil }
Current.currentDate = { currentDate }

let url = URL(
string:
"https://dummy-url.com/callback#access_token=accesstoken&expires_in=60&refresh_token=refreshtoken&token_type=bearer"
)!

let session = try await sut.session(from: url)
let expectedSession = Session(
accessToken: "accesstoken",
tokenType: "bearer",
expiresIn: 60,
expiresAt: currentDate.addingTimeInterval(60).timeIntervalSince1970,
refreshToken: "refreshtoken",
user: User(fromMockNamed: "user")
)
XCTAssertEqual(session, expectedSession)
}
let session = try await sut.session(from: url)
let expectedSession = Session(
accessToken: "accesstoken",
tokenType: "bearer",
expiresIn: 60,
expiresAt: currentDate.addingTimeInterval(60).timeIntervalSince1970,
refreshToken: "refreshtoken",
user: User(fromMockNamed: "user")
)
XCTAssertEqual(session, expectedSession)
}
#endif

func testSessionFromURLWithMissingComponent() async {
let sut = makeSUT()
Expand Down Expand Up @@ -479,3 +481,14 @@ final class RequestsTests: XCTestCase {
)
}
}

extension HTTPURLResponse {
fileprivate static func stub(code: Int = 200) -> HTTPURLResponse {
HTTPURLResponse(
url: clientURL,
statusCode: code,
httpVersion: nil,
headerFields: nil
)!
}
}
Loading