Skip to content

release: apple #64

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

Merged
merged 11 commits into from
Dec 18, 2024
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
11 changes: 11 additions & 0 deletions .github/workflows/autoclose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Auto-close External Pull Requests

on:
pull_request_target:
types: [opened, reopened]

jobs:
auto_close:
uses: appwrite/.github/.github/workflows/autoclose.yml@main
secrets:
GH_AUTO_CLOSE_PR_TOKEN: ${{ secrets.GH_AUTO_CLOSE_PR_TOKEN }}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Add the package to your `Package.swift` dependencies:

```swift
dependencies: [
.package(url: "[email protected]:appwrite/sdk-for-apple.git", from: "7.0.0"),
.package(url: "[email protected]:appwrite/sdk-for-apple.git", from: "7.1.0"),
],
```

Expand Down
16 changes: 8 additions & 8 deletions Sources/Appwrite/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ open class Client {
"x-sdk-name": "Apple",
"x-sdk-platform": "client",
"x-sdk-language": "apple",
"x-sdk-version": "7.0.0",
"x-sdk-version": "7.1.0",
"x-appwrite-response-format": "1.6.0"
]

Expand Down Expand Up @@ -464,23 +464,23 @@ open class Client {
if param is String
|| param is Int
|| param is Float
|| param is Double
|| param is Bool
|| param is [String]
|| param is [Int]
|| param is [Float]
|| param is [Double]
|| param is [Bool]
|| param is [String: Any]
|| param is [Int: Any]
|| param is [Float: Any]
|| param is [Double: Any]
|| param is [Bool: Any] {
encodedParams[key] = param
} else {
let value = try! (param as! Encodable).toJson()

let range = value.index(value.startIndex, offsetBy: 1)..<value.index(value.endIndex, offsetBy: -1)
let substring = value[range]

encodedParams[key] = substring
} else if let encodable = param as? Encodable {
encodedParams[key] = try encodable.toJson()
} else if let param = param {
encodedParams[key] = String(describing: param)
}
}

Expand Down
57 changes: 47 additions & 10 deletions Sources/Appwrite/OAuth/WebAuthComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import AsyncHTTPClient
import Foundation
import NIO

#if canImport(SwiftUI)
import SwiftUI
#if canImport(AuthenticationServices)
import AuthenticationServices
#endif

///
Expand All @@ -13,12 +13,10 @@ import SwiftUI
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *)
public class WebAuthComponent {

#if canImport(SwiftUI)
@Environment(\.openURL)
private static var openURL
#endif

private static var callbacks = [String: (Result<String, AppwriteError>) -> Void]()
#if canImport(AuthenticationServices)
private static var currentAuthSession: ASWebAuthenticationSession?
#endif

///
/// Authenticate Session with OAuth2
Expand All @@ -41,9 +39,29 @@ public class WebAuthComponent {
) {
callbacks[callbackScheme] = onComplete

#if canImport(SwiftUI)
openURL(url)
#endif
#if canImport(AuthenticationServices)
currentAuthSession = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme
) { callbackURL, error in
if let error = error {
cleanUp()
} else if let callbackURL = callbackURL {
// handle cookies here itself!
WebAuthComponent.handleIncomingCookie(from: callbackURL)
cleanUp()
}
}

if let session = currentAuthSession {
/// Indicates that the session should be a private session.
session.prefersEphemeralWebBrowserSession = true
session.presentationContextProvider = PresentationContextProvider.shared
session.start()
} else {
print("Failed to create ASWebAuthenticationSession")
}
#endif
}

///
Expand Down Expand Up @@ -130,5 +148,24 @@ public class WebAuthComponent {
callbacks.forEach { (_, callback) in
callback(.failure(AppwriteError(message: "User cancelled login.")))
}

#if canImport(AuthenticationServices)
currentAuthSession = nil
#endif
}
}

#if canImport(AuthenticationServices)
/// Presentation context for the ASWebAuthenticationSession.
class PresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
static let shared = PresentationContextProvider()

func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
if let mainWindow = OSApplication.shared.windows.first { $0.isKeyWindow } {
return mainWindow
}

return ASPresentationAnchor()
}
}
#endif
42 changes: 25 additions & 17 deletions Sources/Appwrite/Services/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ open class Account: Service {
}

///
/// List Identities
/// List identities
///
/// Get the list of identities for the currently logged in user.
///
Expand Down Expand Up @@ -402,7 +402,7 @@ open class Account: Service {
}

///
/// Create Authenticator
/// Create authenticator
///
/// Add an authenticator app to be used as an MFA factor. Verify the
/// authenticator using the [verify
Expand Down Expand Up @@ -439,7 +439,7 @@ open class Account: Service {
}

///
/// Verify Authenticator
/// Verify authenticator
///
/// Verify an authenticator app after adding it using the [add
/// authenticator](/docs/references/cloud/client-web/account#createMfaAuthenticator)
Expand Down Expand Up @@ -480,7 +480,7 @@ open class Account: Service {
}

///
/// Verify Authenticator
/// Verify authenticator
///
/// Verify an authenticator app after adding it using the [add
/// authenticator](/docs/references/cloud/client-web/account#createMfaAuthenticator)
Expand All @@ -503,7 +503,7 @@ open class Account: Service {
}

///
/// Delete Authenticator
/// Delete authenticator
///
/// Delete an authenticator for a user by ID.
///
Expand Down Expand Up @@ -531,7 +531,7 @@ open class Account: Service {
}

///
/// Create MFA Challenge
/// Create MFA challenge
///
/// Begin the process of MFA verification after sign-in. Finish the flow with
/// [updateMfaChallenge](/docs/references/cloud/client-web/account#updateMfaChallenge)
Expand Down Expand Up @@ -568,7 +568,7 @@ open class Account: Service {
}

///
/// Create MFA Challenge (confirmation)
/// Create MFA challenge (confirmation)
///
/// Complete the MFA challenge by providing the one-time password. Finish the
/// process of MFA verification by providing the one-time password. To begin
Expand Down Expand Up @@ -604,7 +604,7 @@ open class Account: Service {
}

///
/// List Factors
/// List factors
///
/// List the factors available on the account to be used as a MFA challange.
///
Expand Down Expand Up @@ -635,7 +635,7 @@ open class Account: Service {
}

///
/// Get MFA Recovery Codes
/// Get MFA recovery codes
///
/// Get recovery codes that can be used as backup for MFA flow. Before getting
/// codes, they must be generated using
Expand Down Expand Up @@ -669,7 +669,7 @@ open class Account: Service {
}

///
/// Create MFA Recovery Codes
/// Create MFA recovery codes
///
/// Generate recovery codes as backup for MFA flow. It's recommended to
/// generate and show then immediately after user successfully adds their
Expand Down Expand Up @@ -704,7 +704,7 @@ open class Account: Service {
}

///
/// Regenerate MFA Recovery Codes
/// Regenerate MFA recovery codes
///
/// Regenerate recovery codes that can be used as backup for MFA flow. Before
/// regenerating codes, they must be first generated using
Expand Down Expand Up @@ -1350,12 +1350,16 @@ open class Account: Service {
let callbackScheme = "appwrite-callback-\(client.config["project"] ?? "")"

_ = try await withCheckedThrowingContinuation { continuation in
WebAuthComponent.authenticate(url: url, callbackScheme: callbackScheme) { result in
continuation.resume(with: result)
/// main thread for PresentationContextProvider
DispatchQueue.main.async {
WebAuthComponent.authenticate(url: url, callbackScheme: callbackScheme) { result in
continuation.resume(with: result)
}
}
}

return true

}

///
Expand Down Expand Up @@ -1849,12 +1853,16 @@ open class Account: Service {
let callbackScheme = "appwrite-callback-\(client.config["project"] ?? "")"

_ = try await withCheckedThrowingContinuation { continuation in
WebAuthComponent.authenticate(url: url, callbackScheme: callbackScheme) { result in
continuation.resume(with: result)
/// main thread for PresentationContextProvider
DispatchQueue.main.async {
WebAuthComponent.authenticate(url: url, callbackScheme: callbackScheme) { result in
continuation.resume(with: result)
}
}
}

return true

}

///
Expand Down
2 changes: 1 addition & 1 deletion Sources/Appwrite/Services/Locale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ open class Locale: Service {
}

///
/// List Locale Codes
/// List locale codes
///
/// List of all locale codes in [ISO
/// 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
Expand Down
33 changes: 33 additions & 0 deletions Sources/Appwrite/Services/Realtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ open class Realtime : Service {

private let TYPE_ERROR = "error"
private let TYPE_EVENT = "event"
private let TYPE_PONG = "pong"
private let DEBOUNCE_NANOS = 1_000_000
private let HEARTBEAT_INTERVAL: UInt64 = 20_000_000_000 // 20 seconds in nanoseconds

private var socketClient: WebSocketClient? = nil
private var activeChannels = Set<String>()
private var activeSubscriptions = [Int: RealtimeCallback]()
private var heartbeatTask: Task<Void, Swift.Error>? = nil

let connectSync = DispatchQueue(label: "ConnectSync")

Expand All @@ -20,6 +23,29 @@ open class Realtime : Service {
private var subscriptionsCounter = 0
private var reconnect = true

private func startHeartbeat() {
stopHeartbeat()
heartbeatTask = Task {
do {
while !Task.isCancelled {
if let client = socketClient, client.isConnected {
client.send(text: #"{"type": "ping"}"#)
}
try await Task.sleep(nanoseconds: HEARTBEAT_INTERVAL)
}
} catch {
if !Task.isCancelled {
print("Heartbeat task failed: \(error.localizedDescription)")
}
}
}
}

private func stopHeartbeat() {
heartbeatTask?.cancel()
heartbeatTask = nil
}

private func createSocket() async throws {
guard activeChannels.count > 0 else {
reconnect = false
Expand Down Expand Up @@ -50,6 +76,8 @@ open class Realtime : Service {
}

private func closeSocket() async throws {
stopHeartbeat()

guard let client = socketClient,
let group = client.threadGroup else {
return
Expand Down Expand Up @@ -163,6 +191,7 @@ extension Realtime: WebSocketClientDelegate {

public func onOpen(channel: Channel) {
self.reconnectAttempts = 0
startHeartbeat()
}

public func onMessage(text: String) {
Expand All @@ -172,13 +201,16 @@ extension Realtime: WebSocketClientDelegate {
switch type {
case TYPE_ERROR: try! handleResponseError(from: json)
case TYPE_EVENT: handleResponseEvent(from: json)
case TYPE_PONG: break // Handle pong response if needed
default: break
}
}
}
}

public func onClose(channel: Channel, data: Data) async throws {
stopHeartbeat()

if (!reconnect) {
reconnect = true
return
Expand All @@ -196,6 +228,7 @@ extension Realtime: WebSocketClientDelegate {
}

public func onError(error: Swift.Error?, status: HTTPResponseStatus?) {
stopHeartbeat()
print(error?.localizedDescription ?? "Unknown error")
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Appwrite/Services/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ open class Storage: Service {
}

///
/// Delete File
/// Delete file
///
/// Delete a file by its unique ID. Only users with write permissions have
/// access to delete this resource.
Expand Down
Loading