diff --git a/Package.swift b/Package.swift index 91daadf..c7ba17d 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.17.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.32.0"), ], targets: [ diff --git a/README.md b/README.md index a5fff06..53dcbcb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Swift Package Manager](https://img.shields.io/github/v/release/appwrite/sdk-for-apple.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-apple.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.5.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.5.6-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) @@ -31,7 +31,7 @@ Add the package to your `Package.swift` dependencies: ```swift dependencies: [ - .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "5.0.0"), + .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "6.0.0"), ], ``` diff --git a/Sources/Appwrite/Client.swift b/Sources/Appwrite/Client.swift index 6c1d020..ea34ded 100644 --- a/Sources/Appwrite/Client.swift +++ b/Sources/Appwrite/Client.swift @@ -23,7 +23,7 @@ open class Client { "x-sdk-name": "Apple", "x-sdk-platform": "client", "x-sdk-language": "apple", - "x-sdk-version": "5.0.0", + "x-sdk-version": "6.0.0", "x-appwrite-response-format": "1.5.0" ] @@ -577,6 +577,8 @@ extension Client { return "tvos" #elseif os(macOS) return "macos" + #elseif os(visionOS) + return "visionos" #elseif os(Linux) return "linux" #elseif os(Windows) diff --git a/Sources/Appwrite/DeviceInfo/OSDeviceInfo.swift b/Sources/Appwrite/DeviceInfo/OSDeviceInfo.swift index 9b7d51d..58c0660 100644 --- a/Sources/Appwrite/DeviceInfo/OSDeviceInfo.swift +++ b/Sources/Appwrite/DeviceInfo/OSDeviceInfo.swift @@ -2,7 +2,7 @@ import Foundation class OSDeviceInfo { - #if os(iOS) || os(tvOS) + #if os(iOS) || os(tvOS) || os(visionOS) var iOSInfo: IOSDeviceInfo? #elseif os(watchOS) var watchOSInfo: WatchOSDeviceInfo? @@ -15,7 +15,7 @@ class OSDeviceInfo { #endif init() { - #if os(iOS) || os(tvOS) + #if os(iOS) || os(tvOS) || os(visionOS) self.iOSInfo = IOSDeviceInfo() #elseif os(watchOS) self.watchOSInfo = WatchOSDeviceInfo() diff --git a/Sources/Appwrite/DeviceInfo/iOS/IOSDeviceInfo.swift b/Sources/Appwrite/DeviceInfo/iOS/IOSDeviceInfo.swift index b9d9534..219b958 100644 --- a/Sources/Appwrite/DeviceInfo/iOS/IOSDeviceInfo.swift +++ b/Sources/Appwrite/DeviceInfo/iOS/IOSDeviceInfo.swift @@ -1,4 +1,4 @@ -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || os(visionOS) import Foundation import UIKit diff --git a/Sources/Appwrite/DeviceInfo/iOS/UIDevice+ModelName.swift b/Sources/Appwrite/DeviceInfo/iOS/UIDevice+ModelName.swift index 38d7618..ee66cb7 100644 --- a/Sources/Appwrite/DeviceInfo/iOS/UIDevice+ModelName.swift +++ b/Sources/Appwrite/DeviceInfo/iOS/UIDevice+ModelName.swift @@ -1,4 +1,4 @@ -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || os(visionOS) import Foundation import UIKit @@ -82,7 +82,7 @@ public extension UIDevice { case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)" case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)" case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)" - case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))" + case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))" default: return identifier #elseif os(tvOS) case "AppleTV1,1": return "Apple TV (1st generation)" @@ -91,8 +91,12 @@ public extension UIDevice { case "AppleTV5,3": return "Apple TV (4th generation)" case "AppleTV6,2": return "Apple TV 4K (1st generation)" case "AppleTV11,1": return "Apple TV 4K (2nd generation)" - case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))" - default: return identifier + case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))" + default: return identifier + #elseif os(visionOS) + case "RealityDevice14,1": return "Apple Vision Pro" + case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "visionOS"))" + default: return identifier #endif } } diff --git a/Sources/Appwrite/ID.swift b/Sources/Appwrite/ID.swift index 4641f2b..3abd379 100644 --- a/Sources/Appwrite/ID.swift +++ b/Sources/Appwrite/ID.swift @@ -1,9 +1,26 @@ +import Foundation + public class ID { + // Generate an hex ID based on timestamp + // Recreated from https://www.php.net/manual/en/function.uniqid.php + private static func hexTimestamp() -> String { + let now = Date() + let secs = Int(now.timeIntervalSince1970) + let usec = Int((now.timeIntervalSince1970 - Double(secs)) * 1_000_000) + let hexTimestamp = String(format: "%08x%05x", secs, usec) + return hexTimestamp + } + public static func custom(_ id: String) -> String { return id } - public static func unique() -> String { - return "unique()" + // Generate a unique ID with padding to have a longer ID + public static func unique(padding: Int = 7) -> String { + let baseId = Self.hexTimestamp() + let randomPadding = (1...padding).map { + _ in String(format: "%x", Int.random(in: 0..<16)) + }.joined() + return baseId + randomPadding } -} \ No newline at end of file +} diff --git a/Sources/Appwrite/Models/RealtimeModels.swift b/Sources/Appwrite/Models/RealtimeModels.swift index 3a9c3e2..5129b4c 100644 --- a/Sources/Appwrite/Models/RealtimeModels.swift +++ b/Sources/Appwrite/Models/RealtimeModels.swift @@ -1,11 +1,15 @@ import Foundation public class RealtimeSubscription { - public var close: () -> Void + private var close: () async throws -> Void - init(close: @escaping () -> Void) { + init(close: @escaping () async throws-> Void) { self.close = close } + + public func close() async throws { + try await self.close() + } } public class RealtimeCallback { @@ -14,7 +18,7 @@ public class RealtimeCallback { init( for channels: Set, - and callback: @escaping (RealtimeResponseEvent) -> Void + with callback: @escaping (RealtimeResponseEvent) -> Void ) { self.channels = channels self.callback = callback diff --git a/Sources/Appwrite/OAuth/View+OAuth.swift b/Sources/Appwrite/OAuth/View+OAuth.swift index 6285df0..f58ae57 100644 --- a/Sources/Appwrite/OAuth/View+OAuth.swift +++ b/Sources/Appwrite/OAuth/View+OAuth.swift @@ -2,7 +2,7 @@ typealias OSApplication = NSApplication typealias OSViewController = NSViewController let notificationType = NSApplication.willBecomeActiveNotification -#elseif os(iOS) || os(tvOS) +#elseif os(iOS) || os(tvOS) || os(visionOS) typealias OSApplication = UIApplication typealias OSViewController = UIViewController let notificationType = UIApplication.willEnterForegroundNotification @@ -14,7 +14,7 @@ let notificationType = WKApplication.willEnterForegroundNotification #if canImport(SwiftUI) import SwiftUI -@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) extension View { public func registerOAuthHandler() -> some View { onOpenURL { url in @@ -27,12 +27,12 @@ extension View { #endif #if canImport(OSViewController) -@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) extension OSViewController { public func registerOAuthHandler() { #if os(macOS) typealias OSHostingController = NSHostingController - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) typealias OSHostingController = UIHostingController #endif self.addChild(OSHostingController(rootView: EmptyView().registerOAuthHandler())) diff --git a/Sources/Appwrite/OAuth/WebAuthComponent.swift b/Sources/Appwrite/OAuth/WebAuthComponent.swift index 6d4d330..9cb5da4 100644 --- a/Sources/Appwrite/OAuth/WebAuthComponent.swift +++ b/Sources/Appwrite/OAuth/WebAuthComponent.swift @@ -10,7 +10,7 @@ import SwiftUI /// Used to authenticate with external OAuth2 providers. Launches browser windows and handles /// suspension until the user completes the process or otherwise returns to the app. /// -@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) public class WebAuthComponent { #if canImport(SwiftUI) diff --git a/Sources/Appwrite/PackageInfo/OSPackageInfo.swift b/Sources/Appwrite/PackageInfo/OSPackageInfo.swift index 12d441f..f49b956 100644 --- a/Sources/Appwrite/PackageInfo/OSPackageInfo.swift +++ b/Sources/Appwrite/PackageInfo/OSPackageInfo.swift @@ -3,7 +3,7 @@ import Foundation class OSPackageInfo { public static func get() -> PackageInfo { - #if os(iOS) || os(watchOS) || os(tvOS) || os(macOS) + #if os(iOS) || os(watchOS) || os(tvOS) || os(macOS) || os(visionOS) return PackageInfo.getApplePackage() #elseif os(Linux) return PackageInfo.getLinuxPackage() diff --git a/Sources/Appwrite/Services/Account.swift b/Sources/Appwrite/Services/Account.swift index 9594aeb..dff4046 100644 --- a/Sources/Appwrite/Services/Account.swift +++ b/Sources/Appwrite/Services/Account.swift @@ -406,7 +406,7 @@ open class Account: Service { /// /// Add an authenticator app to be used as an MFA factor. Verify the /// authenticator using the [verify - /// authenticator](/docs/references/cloud/client-web/account#verifyAuthenticator) + /// authenticator](/docs/references/cloud/client-web/account#updateMfaAuthenticator) /// method. /// /// @param AppwriteEnums.AuthenticatorType type @@ -442,8 +442,8 @@ open class Account: Service { /// Verify Authenticator /// /// Verify an authenticator app after adding it using the [add - /// authenticator](/docs/references/cloud/client-web/account#addAuthenticator) - /// method. + /// authenticator](/docs/references/cloud/client-web/account#createMfaAuthenticator) + /// method. add /// /// @param AppwriteEnums.AuthenticatorType type /// @param String otp @@ -483,8 +483,8 @@ open class Account: Service { /// Verify Authenticator /// /// Verify an authenticator app after adding it using the [add - /// authenticator](/docs/references/cloud/client-web/account#addAuthenticator) - /// method. + /// authenticator](/docs/references/cloud/client-web/account#createMfaAuthenticator) + /// method. add /// /// @param AppwriteEnums.AuthenticatorType type /// @param String otp @@ -512,11 +512,10 @@ open class Account: Service { /// @throws Exception /// @return array /// - open func deleteMfaAuthenticator( + open func deleteMfaAuthenticator( type: AppwriteEnums.AuthenticatorType, - otp: String, - nestedType: T.Type - ) async throws -> AppwriteModels.User { + otp: String + ) async throws -> Any { let apiPath: String = "/account/mfa/authenticators/{type}" .replacingOccurrences(of: "{type}", with: type.rawValue) @@ -528,38 +527,11 @@ open class Account: Service { "content-type": "application/json" ] - let converter: (Any) -> AppwriteModels.User = { response in - return AppwriteModels.User.from(map: response as! [String: Any]) - } - return try await client.call( method: "DELETE", path: apiPath, headers: apiHeaders, - params: apiParams, - converter: converter - ) - } - - /// - /// Delete Authenticator - /// - /// Delete an authenticator for a user by ID. - /// - /// @param AppwriteEnums.AuthenticatorType type - /// @param String otp - /// @throws Exception - /// @return array - /// - open func deleteMfaAuthenticator( - type: AppwriteEnums.AuthenticatorType, - otp: String - ) async throws -> AppwriteModels.User<[String: AnyCodable]> { - return try await deleteMfaAuthenticator( - type: type, - otp: otp, - nestedType: [String: AnyCodable].self - ) + params: apiParams ) } /// diff --git a/Sources/Appwrite/Services/Realtime.swift b/Sources/Appwrite/Services/Realtime.swift index a02b957..3bc2e9e 100644 --- a/Sources/Appwrite/Services/Realtime.swift +++ b/Sources/Appwrite/Services/Realtime.swift @@ -7,22 +7,23 @@ open class Realtime : Service { private let TYPE_ERROR = "error" private let TYPE_EVENT = "event" - private let DEBOUNCE_MILLIS = 1 + private let DEBOUNCE_NANOS = 1_000_000 private var socketClient: WebSocketClient? = nil private var activeChannels = Set() private var activeSubscriptions = [Int: RealtimeCallback]() let connectSync = DispatchQueue(label: "ConnectSync") - let callbackSync = DispatchQueue(label: "CallbackSync") private var subCallDepth = 0 private var reconnectAttempts = 0 private var subscriptionsCounter = 0 private var reconnect = true - private func createSocket() { + private func createSocket() async throws { guard activeChannels.count > 0 else { + reconnect = false + try await closeSocket() return } @@ -36,17 +37,31 @@ open class Realtime : Service { if (socketClient != nil) { reconnect = false - closeSocket() - } else { - socketClient = WebSocketClient(url, tlsEnabled: !client.selfSigned, delegate: self)! + try await closeSocket() } - try! socketClient?.connect() + socketClient = WebSocketClient( + url, + tlsEnabled: !client.selfSigned, + delegate: self + ) + + try await socketClient?.connect() } - private func closeSocket() { - socketClient?.close() - //socket?.close(RealtimeCode.POLICY_VIOLATION.value, null) + private func closeSocket() async throws { + guard let client = socketClient, + let group = client.threadGroup else { + return + } + + if (client.isConnected) { + let promise = group.any().makePromise(of: Void.self) + client.close(promise: promise) + try await promise.futureResult.get() + } + + try await group.shutdownGracefully() } private func getTimeout() -> Int { @@ -61,8 +76,8 @@ open class Realtime : Service { public func subscribe( channel: String, callback: @escaping (RealtimeResponseEvent) -> Void - ) -> RealtimeSubscription { - return subscribe( + ) async throws -> RealtimeSubscription { + return try await subscribe( channels: [channel], payloadType: String.self, callback: callback @@ -72,8 +87,8 @@ open class Realtime : Service { public func subscribe( channels: Set, callback: @escaping (RealtimeResponseEvent) -> Void - ) -> RealtimeSubscription { - return subscribe( + ) async throws -> RealtimeSubscription { + return try await subscribe( channels: channels, payloadType: String.self, callback: callback @@ -84,8 +99,8 @@ open class Realtime : Service { channel: String, payloadType: T.Type, callback: @escaping (RealtimeResponseEvent) -> Void - ) -> RealtimeSubscription { - return subscribe( + ) async throws -> RealtimeSubscription { + return try await subscribe( channels: [channel], payloadType: T.self, callback: callback @@ -96,36 +111,38 @@ open class Realtime : Service { channels: Set, payloadType: T.Type, callback: @escaping (RealtimeResponseEvent) -> Void - ) -> RealtimeSubscription { + ) async throws -> RealtimeSubscription { subscriptionsCounter += 1 - let counter = subscriptionsCounter + + let count = subscriptionsCounter channels.forEach { activeChannels.insert($0) } - activeSubscriptions[counter] = RealtimeCallback( + activeSubscriptions[count] = RealtimeCallback( for: Set(channels), - and: callback + with: callback ) connectSync.sync { subCallDepth+=1 } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(DEBOUNCE_MILLIS)) { - if (self.subCallDepth == 1) { - self.createSocket() - } - self.connectSync.sync { - self.subCallDepth-=1 - } + try await Task.sleep(nanoseconds: UInt64(DEBOUNCE_NANOS)) + + if self.subCallDepth == 1 { + try await self.createSocket() + } + + connectSync.sync { + self.subCallDepth -= 1 } return RealtimeSubscription { - self.activeSubscriptions[counter] = nil + self.activeSubscriptions[count] = nil self.cleanUp(channels: channels) - self.createSocket() + try await self.createSocket() } } @@ -137,7 +154,7 @@ open class Realtime : Service { let subsWithChannel = activeSubscriptions.filter { callback in return callback.value.channels.contains(channel) } - return subsWithChannel.isEmpty + return !subsWithChannel.isEmpty } } } @@ -161,7 +178,7 @@ extension Realtime: WebSocketClientDelegate { } } - public func onClose(channel: Channel, data: Data) { + public func onClose(channel: Channel, data: Data) async throws { if (!reconnect) { reconnect = true return @@ -171,10 +188,11 @@ extension Realtime: WebSocketClientDelegate { print("Realtime disconnected. Re-connecting in \(timeout / 1000) seconds.") - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(timeout)) { - self.reconnectAttempts += 1 - self.createSocket() - } + try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000)) + + self.reconnectAttempts += 1 + + try await self.createSocket() } public func onError(error: Swift.Error?, status: HTTPResponseStatus?) { @@ -186,16 +204,10 @@ extension Realtime: WebSocketClientDelegate { } func handleResponseEvent(from json: [String: Any]) { - guard let data = json["data"] as? [String: Any] else { - return - } - guard let channels = data["channels"] as? Array else { - return - } - guard let events = data["events"] as? Array else { - return - } - guard let payload = data["payload"] as? [String: Any] else { + guard let data = json["data"] as? [String: Any], + let channels = data["channels"] as? [String], + let events = data["events"] as? [String], + let payload = data["payload"] as? [String: Any] else { return } guard channels.contains(where: { channel in diff --git a/Sources/Appwrite/WebSockets/WebSocketClient.swift b/Sources/Appwrite/WebSockets/WebSocketClient.swift index 4da31d7..72322b7 100644 --- a/Sources/Appwrite/WebSockets/WebSocketClient.swift +++ b/Sources/Appwrite/WebSockets/WebSocketClient.swift @@ -7,6 +7,8 @@ import NIOFoundationCompat import NIOSSL public let WEBSOCKET_LOCKER_QUEUE = "SyncLocker" +public let WEBSOCKET_THREAD_QUEUE = "ThreadLocker" +public let WEBSOCKET_CHANNEL_QUEUE = "ChannelLocker" /// Creates and manages connections to a WebSocket server. /// @@ -20,16 +22,35 @@ public class WebSocketClient { let query: String let headers: HTTPHeaders let frameKey: String - + public private(set) var maxFrameSize: Int - - var channel: Channel? = nil + var tlsEnabled: Bool = false var closeSent: Bool = false - let locker = DispatchQueue(label: WEBSOCKET_LOCKER_QUEUE, qos: .background) + private let locker = DispatchQueue(label: WEBSOCKET_LOCKER_QUEUE, qos: .background) + private let channelQueue = DispatchQueue(label: WEBSOCKET_CHANNEL_QUEUE) + private let threadGroupQueue = DispatchQueue(label: WEBSOCKET_THREAD_QUEUE) - var threadGroup: MultiThreadedEventLoopGroup? = nil + var channel: Channel? { + get { + return channelQueue.sync { _channel } + } + set { + channelQueue.sync { _channel = newValue } + } + } + private var _channel: Channel? = nil + + var threadGroup: MultiThreadedEventLoopGroup? { + get { + return threadGroupQueue.sync { _threadGroup } + } + set { + threadGroupQueue.sync { _threadGroup = newValue } + } + } + private var _threadGroup: MultiThreadedEventLoopGroup? weak var delegate: WebSocketClientDelegate? = nil @@ -216,43 +237,45 @@ public class WebSocketClient { self.threadGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) } } - - deinit { - try! threadGroup!.syncShutdownGracefully() - } // MARK: - Open connection - + /// Open a connection to the configured host and attempt to upgrade the connection to a WebSocket. If successful the `onOpen` callback will fire, otherwise a connection error will be thrown from here. - public func connect() throws { + public func connect() async throws { let socketOptions = ChannelOptions.socket( SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT ) - while(threadGroup == nil) {} - + while(threadGroup == nil) { + try? await Task.sleep(nanoseconds: 10_000_000) + } + let bootstrap = ClientBootstrap(group: threadGroup!) .channelOption(socketOptions, value: 1) - .channelInitializer(self.openChannel) - - _ = try bootstrap - .connect(host: self.host, port: self.port) - .wait() + .channelInitializer { + self.openChannel(channel: $0) + } + + _ = try await bootstrap + .connect(host: self.host,port: self.port) + .get() } private func openChannel(channel: Channel) -> EventLoopFuture { let httpHandler = HTTPHandler(client: self, headers: headers) - + let basicUpgrader = NIOWebSocketClientUpgrader( requestKey: self.frameKey, - upgradePipelineHandler: self.upgradePipelineHandler + upgradePipelineHandler: { channel, response in + self.upgradePipelineHandler(channel: channel, response: response) + } ) - + let config: NIOHTTPClientUpgradeConfiguration = (upgraders: [basicUpgrader], completionHandler: { context in context.channel.pipeline.removeHandler(httpHandler, promise: nil) }) - + return channel.pipeline.addHTTPClientHandlers(withClientUpgrade: config).flatMap { _ in return channel.pipeline.addHandler(httpHandler).flatMap { _ in if self.tlsEnabled { @@ -267,39 +290,43 @@ public class WebSocketClient { } } - @Sendable private func upgradePipelineHandler(channel: Channel, response: HTTPResponseHead) -> EventLoopFuture { + private func upgradePipelineHandler(channel: Channel, response: HTTPResponseHead) -> EventLoopFuture { let handler = MessageHandler(client: self) - + if response.status == .switchingProtocols { self.channel = channel } - + return channel.pipeline.addHandler(handler) } // MARK: - Close connection - + /// Closes the connection /// /// - parameters: /// - data: Close frame payload - public func close(data: Data = Data()) { + public func close( + data: Data = Data(), + promise: EventLoopPromise? = nil + ) { closeSent = true - + var buffer = ByteBufferAllocator() .buffer(capacity: data.count) - + buffer.writeBytes(data) - + send( data: buffer, opcode: .connectionClose, - finalFrame: true + finalFrame: true, + promise: promise ) } - + // MARK: - Send data - + /// Sends binary-formatted data to the connected server in multiple frames. /// /// - parameters: @@ -309,21 +336,23 @@ public class WebSocketClient { public func send( data: Data, opcode: WebSocketOpcode, - finalFrame: Bool = true + finalFrame: Bool = true, + promise: EventLoopPromise? = nil ) { var buffer = ByteBufferAllocator() .buffer(capacity: data.count) - + buffer.writeBytes(data) - + if opcode == .connectionClose { self.closeSent = true } - + send( data: buffer, opcode: opcode, - finalFrame: finalFrame + finalFrame: finalFrame, + promise: promise ) } @@ -336,21 +365,23 @@ public class WebSocketClient { public func send( text: String, opcode: WebSocketOpcode = .text, - finalFrame: Bool = true + finalFrame: Bool = true, + promise: EventLoopPromise? = nil ) { var buffer = ByteBufferAllocator() .buffer(capacity: text.count) - + buffer.writeString(text) - + send( data: buffer, opcode: opcode, - finalFrame: finalFrame + finalFrame: finalFrame, + promise: promise ) } - + /// Sends the JSON representation of the given model to the connected server in multiple frames. /// /// - parameters: @@ -360,7 +391,8 @@ public class WebSocketClient { public func send( model: T, opcode: WebSocketOpcode = .text, - finalFrame: Bool = true + finalFrame: Bool = true, + promise: EventLoopPromise? = nil ) { let jsonEncoder = JSONEncoder() do { @@ -368,13 +400,14 @@ public class WebSocketClient { let string = String(data: jsonData, encoding: .utf8)! var buffer = ByteBufferAllocator() .buffer(capacity: string.count) - + buffer.writeString(string) - + send( data: buffer, opcode: opcode, - finalFrame: finalFrame + finalFrame: finalFrame, + promise: promise ) } catch let error { print(error) @@ -390,7 +423,8 @@ public class WebSocketClient { public func send( data: ByteBuffer, opcode: WebSocketOpcode, - finalFrame: Bool + finalFrame: Bool, + promise: EventLoopPromise? = nil ) { let frame = WebSocketFrame( fin: finalFrame, @@ -398,13 +432,19 @@ public class WebSocketClient { maskKey: nil, data: data ) + guard let channel = channel else { return } + if finalFrame { - channel.writeAndFlush(frame, promise: nil) + channel.writeAndFlush(frame, promise: promise) } else { - channel.write(frame, promise: nil) + channel.write(frame, promise: promise) + } + + if opcode == .connectionClose { + channel.close(mode: .all, promise: promise) } } } diff --git a/Sources/AppwriteEnums/CreditCard.swift b/Sources/AppwriteEnums/CreditCard.swift index 9296562..3720b54 100644 --- a/Sources/AppwriteEnums/CreditCard.swift +++ b/Sources/AppwriteEnums/CreditCard.swift @@ -4,7 +4,7 @@ public enum CreditCard: String, Codable { case americanExpress = "amex" case argencard = "argencard" case cabal = "cabal" - case consosud = "censosud" + case cencosud = "cencosud" case dinersClub = "diners" case discover = "discover" case elo = "elo" diff --git a/Sources/AppwriteEnums/Flag.swift b/Sources/AppwriteEnums/Flag.swift index 11f1fe8..c577fcf 100644 --- a/Sources/AppwriteEnums/Flag.swift +++ b/Sources/AppwriteEnums/Flag.swift @@ -142,6 +142,7 @@ public enum Flag: String, Codable { case palau = "pw" case papuaNewGuinea = "pg" case poland = "pl" + case frenchPolynesia = "pf" case northKorea = "kp" case portugal = "pt" case paraguay = "py" diff --git a/Sources/AppwriteModels/MfaFactors.swift b/Sources/AppwriteModels/MfaFactors.swift index a4f318a..716c055 100644 --- a/Sources/AppwriteModels/MfaFactors.swift +++ b/Sources/AppwriteModels/MfaFactors.swift @@ -4,31 +4,37 @@ import JSONCodable /// MFAFactors public class MfaFactors { - /// TOTP + /// Can TOTP be used for MFA challenge for this account. public let totp: Bool - /// Phone + /// Can phone (SMS) be used for MFA challenge for this account. public let phone: Bool - /// Email + /// Can email be used for MFA challenge for this account. public let email: Bool + /// Can recovery code be used for MFA challenge for this account. + public let recoveryCode: Bool + init( totp: Bool, phone: Bool, - email: Bool + email: Bool, + recoveryCode: Bool ) { self.totp = totp self.phone = phone self.email = email + self.recoveryCode = recoveryCode } public func toMap() -> [String: Any] { return [ "totp": totp as Any, "phone": phone as Any, - "email": email as Any + "email": email as Any, + "recoveryCode": recoveryCode as Any ] } @@ -36,7 +42,8 @@ public class MfaFactors { return MfaFactors( totp: map["totp"] as! Bool, phone: map["phone"] as! Bool, - email: map["email"] as! Bool + email: map["email"] as! Bool, + recoveryCode: map["recoveryCode"] as! Bool ) } } diff --git a/Sources/AppwriteModels/Session.swift b/Sources/AppwriteModels/Session.swift index c8a9a39..7214468 100644 --- a/Sources/AppwriteModels/Session.swift +++ b/Sources/AppwriteModels/Session.swift @@ -10,6 +10,9 @@ public class Session { /// Session creation date in ISO 8601 format. public let createdAt: String + /// Session update date in ISO 8601 format. + public let updatedAt: String + /// User ID. public let userId: String @@ -92,6 +95,7 @@ public class Session { init( id: String, createdAt: String, + updatedAt: String, userId: String, expire: String, provider: String, @@ -121,6 +125,7 @@ public class Session { ) { self.id = id self.createdAt = createdAt + self.updatedAt = updatedAt self.userId = userId self.expire = expire self.provider = provider @@ -153,6 +158,7 @@ public class Session { return [ "$id": id as Any, "$createdAt": createdAt as Any, + "$updatedAt": updatedAt as Any, "userId": userId as Any, "expire": expire as Any, "provider": provider as Any, @@ -186,6 +192,7 @@ public class Session { return Session( id: map["$id"] as! String, createdAt: map["$createdAt"] as! String, + updatedAt: map["$updatedAt"] as! String, userId: map["userId"] as! String, expire: map["expire"] as! String, provider: map["provider"] as! String, diff --git a/docs/examples/account/delete-mfa-authenticator.md b/docs/examples/account/delete-mfa-authenticator.md index 262f23b..8fef773 100644 --- a/docs/examples/account/delete-mfa-authenticator.md +++ b/docs/examples/account/delete-mfa-authenticator.md @@ -7,7 +7,7 @@ let client = Client() let account = Account(client) -let user = try await account.deleteMfaAuthenticator( +let result = try await account.deleteMfaAuthenticator( type: .totp, otp: "" ) diff --git a/example-swiftui/Shared/ExampleView.swift b/example-swiftui/Shared/ExampleView.swift index d2f6354..0c3d2de 100644 --- a/example-swiftui/Shared/ExampleView.swift +++ b/example-swiftui/Shared/ExampleView.swift @@ -20,6 +20,9 @@ struct ExampleView: View { TextField("", text: $viewModel.response, axis: .vertical) .padding() + TextField("", text: $viewModel.response2, axis: .vertical) + .padding() + Button("Login") { Task { await viewModel.login() } } @@ -41,7 +44,7 @@ struct ExampleView: View { } Button("Subscribe") { - viewModel.subscribe() + Task { await viewModel.subscribe() } } } #if os(macOS) @@ -49,7 +52,7 @@ struct ExampleView: View { ImagePicker.present() } #endif - #if os(iOS) + #if os(iOS) || os(visionOS) .sheet(isPresented: $viewModel.isShowPhotoLibrary) { ImagePicker(selectedImage: $imageToUpload) } diff --git a/example-swiftui/Shared/ExampleViewModel.swift b/example-swiftui/Shared/ExampleViewModel.swift index 817a906..3c6de09 100644 --- a/example-swiftui/Shared/ExampleViewModel.swift +++ b/example-swiftui/Shared/ExampleViewModel.swift @@ -15,8 +15,10 @@ extension ExampleView { @Published public var fileId: String = "test" @Published public var databaseId: String = "test" @Published public var collectionId: String = "test" + @Published public var collectionId2: String = "test2" @Published public var isShowPhotoLibrary = false @Published public var response: String = "" + @Published public var response2: String = "" func register() async { do { @@ -127,13 +129,25 @@ extension ExampleView { } } } - - func subscribe() { - _ = realtime.subscribe(channels: ["databases.\(databaseId).collections.\(collectionId).documents"]) { event in + + func subscribe() async { + let sub1 = try? await realtime.subscribe(channels: ["databases.\(databaseId).collections.\(collectionId).documents"]) { event in DispatchQueue.main.async { self.response = String(describing: event.payload!) } } + + try? await Task.sleep(nanoseconds: UInt64(500_000_000)) + + _ = try? await realtime.subscribe(channels: ["databases.\(databaseId).collections.\(collectionId2).documents"]) { event in + DispatchQueue.main.async { + self.response2 = String(describing: event.payload!) + } + } + + try? await Task.sleep(nanoseconds: UInt64(500_000_000)) + + try? await sub1?.close() } } } diff --git a/example-swiftui/Shared/Image/OSImage.swift b/example-swiftui/Shared/Image/OSImage.swift index 11b8d73..c09aa6f 100644 --- a/example-swiftui/Shared/Image/OSImage.swift +++ b/example-swiftui/Shared/Image/OSImage.swift @@ -9,7 +9,7 @@ import SwiftUI #if os(macOS) import AppKit public typealias OSImage = NSImage -#elseif os(iOS) || os(tvOS) || os(watchOS) +#elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) import UIKit public typealias OSImage = UIImage #endif @@ -18,7 +18,7 @@ extension Image { public init(data: Data) { #if os(macOS) self.init(nsImage: NSImage(data: data)!) - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) self.init(uiImage: UIImage(data: data)!) #endif } @@ -28,7 +28,7 @@ extension OSImage { public var data: Data { #if os(macOS) return self.tiffRepresentation! - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) return self.pngData()! #endif }