diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..05b8f44 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +.gitattributes eol=lf +.gitignore eol=lf +*.hh eol=lf +*.json eol=lf +*.plist eol=lf +*.swift eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1177aee --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.sw? +.build/ +.vscode/ +Examples/FireBaseUI/Resources/google-services-desktop.json diff --git a/Examples/FireBaseUI/FireBaseUI.exe.manifest b/Examples/FireBaseUI/FireBaseUI.exe.manifest new file mode 100644 index 0000000..5a4e4f2 --- /dev/null +++ b/Examples/FireBaseUI/FireBaseUI.exe.manifest @@ -0,0 +1,37 @@ + + + + + + + + + + PerMonitorV2 + true + + + + FireBaseUI + + + + + + + + diff --git a/Examples/FireBaseUI/FireBaseUI.swift b/Examples/FireBaseUI/FireBaseUI.swift new file mode 100644 index 0000000..7a61758 --- /dev/null +++ b/Examples/FireBaseUI/FireBaseUI.swift @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import SwiftWin32 +import Foundation + +import firebase +import FirebaseCore +import FirebaseAuth + +extension Foundation.Bundle { + internal static var resources: URL { + Bundle.module.bundleURL.appendingPathComponent("Resources") + } +} + +@main +final class FireBaseUI: ApplicationDelegate { + func application(_ application: Application, + didFinishLaunchingWithOptions: [Application.LaunchOptionsKey:Any]?) + -> Bool { +#if _runtime(_ObjC) + firebase.App.SetDefaultConfigPath(Bundle.resources.fileSystemRepresentation) +#else + Bundle.resources.withUnsafeFileSystemRepresentation(firebase.App.SetDefaultConfigPath) +#endif + + FirebaseApp.configure() + return true + } +} diff --git a/Examples/FireBaseUI/FireBaseUIViewController.swift b/Examples/FireBaseUI/FireBaseUIViewController.swift new file mode 100644 index 0000000..34bf22c --- /dev/null +++ b/Examples/FireBaseUI/FireBaseUIViewController.swift @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase +import FirebaseCore +import FirebaseAuth +import SwiftWin32 + +private final class FireBaseLogLevelPickerHandler { + private let levels = ["Default", "Verbose", "Debug", "Info", "Warning", "Error", "Assert"] +} + +extension FireBaseLogLevelPickerHandler: PickerViewDataSource { + public func numberOfComponents(in pickerView: PickerView) -> Int { + 1 + } + + public func pickerView(_ pickerView: PickerView, + numberOfRowsInComponent component: Int) -> Int { + self.levels.count + } +} + +extension FireBaseLogLevelPickerHandler: PickerViewDelegate { + public func pickerView(_ pickerView: PickerView, titleForRow row: Int, + forComponent component: Int) -> String? { + self.levels[row] + } + + public func pickerView(_ pickerView: PickerView, didSelectRow row: Int, + inComponent component: Int) { + guard row > 0 else { return } + FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel(rawValue: CInt(row - 1))) + } +} + +// MARK: - FireBaseUIViewController + +internal final class FireBaseUIViewController: ViewController { + fileprivate let firebaseLogHandler = FireBaseLogLevelPickerHandler() + + var cboLogLevel = PickerView(frame: Rect(x: 136, y: 8, width: 256, height: 24)) + var txtEmail = TextField(frame: Rect(x: 136, y: 46, width: 512, height: 24)) + var txtPassword = TextField(frame: Rect(x: 136, y: 78, width: 512, height: 24)) + var btnSignIn = Button(frame: Rect(x: 8, y: 116, width: 120, height: 32), + title: "Sign In") + var btnToken = Button(frame: Rect(x: 8, y: 180, width: 120, height: 32), + title: "Get Token") + var chkRefresh = Switch(frame: Rect(x: 8, y: 212, width: 648, height: 32), + title: "Force Token Refresh") + var txtToken: TextView = TextView(frame: Rect(x: 8, y: 244, width: 640, height: 48)) + + var btnCreate = Button(frame: Rect(x: 8, y: 324, width: 120, height: 32), + title: "Create User") + var btnVerify = Button(frame: Rect(x: 132, y: 324, width: 120, height: 32), + title: "Verify Email") + var btnReset = Button(frame: Rect(x: 256, y: 324, width: 156, height: 32), + title: "Reset Password") + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "FireBase UI" + configureView() + + if let user = Auth.auth().currentUser { + txtEmail.text = user.email + try? Auth.auth().signOut() + } + } + + private func configureView() { + let lblLogLevel = Label(frame: Rect(x: 8, y: 8, width: 128, height: 24), + title: "Log Level:") + self.view?.addSubview(lblLogLevel) + + cboLogLevel.dataSource = firebaseLogHandler + cboLogLevel.delegate = firebaseLogHandler + self.view?.addSubview(cboLogLevel) + cboLogLevel.reloadAllComponents() + cboLogLevel.selectRow(0, inComponent: 0, animated: false) + + let lblEmail = Label(frame: Rect(x: 8, y: 46, width: 128, height: 20), + title: "Email Address:") + self.view?.addSubview(lblEmail) + self.view?.addSubview(txtEmail) + + let lblPassword = Label(frame: Rect(x: 8, y: 78, width: 128, height: 20), + title: "Password:") + self.view?.addSubview(lblPassword) + + txtPassword.isSecureTextEntry = true + self.view?.addSubview(txtPassword) + + btnSignIn.addTarget(self, action: FireBaseUIViewController.signIn, + for: .primaryActionTriggered) + self.view?.addSubview(btnSignIn) + + btnToken.addTarget(self, action: FireBaseUIViewController.getToken, + for: .primaryActionTriggered) + self.view?.addSubview(btnToken) + + self.view?.addSubview(chkRefresh) + + txtToken.editable = false + txtToken.font = .systemFont(ofSize: 9) + self.view?.addSubview(txtToken) + + btnCreate.addTarget(self, action: FireBaseUIViewController.createUser, + for: .primaryActionTriggered) + self.view?.addSubview(btnCreate) + + btnVerify.addTarget(self, action: FireBaseUIViewController.verifyEmail, + for: .primaryActionTriggered) + self.view?.addSubview(btnVerify) + + btnReset.addTarget(self, action: FireBaseUIViewController.resetPassword, + for: .primaryActionTriggered) + self.view?.addSubview(btnReset) + } + + private func signIn() { + guard let email = txtEmail.text, let password = txtPassword.text else { + print("email and password are required") + return + } + + Task { + do { + _ = try await Auth.auth().signIn(withEmail: email, password: password) + } catch { + print("Error signing in: \(error.localizedDescription)") + } + } + } + + private func createUser() { + guard let email = txtEmail.text, let password = txtPassword.text else { + print("email and password are required") + return + } + + Task { + do { + _ = try await Auth.auth().createUser(withEmail: email, password: password) + } catch { + print("Error signing in: \(error.localizedDescription)") + } + } + } + + private func getToken() { + Task { + guard var user = Auth.auth().currentUser else { + print("user not logged in") + return + } + + if chkRefresh.isOn { + do { + let result = try await user.getIDTokenResult(forcingRefresh: true) + txtToken.text = result.token + } catch { + print("Error refreshing token: \(error.localizedDescription)") + } + } else { + do { + txtToken.text = try await user.getIDToken() + } catch { + print("Error refreshing token: \(error.localizedDescription)") + } + } + } + } + + private func verifyEmail() { + Task { + guard var user = Auth.auth().currentUser else { + print("user not logged in") + return + } + + try await user.sendEmailVerification() + } + } + + private func resetPassword() { + guard let email = txtEmail.text else { + print("email is required") + return + } + + Task { + do { + _ = try await Auth.auth().sendPasswordReset(withEmail: email) + } catch { + print("Error sending password reset: \(error.localizedDescription)") + } + } + } +} diff --git a/Examples/FireBaseUI/Info.plist b/Examples/FireBaseUI/Info.plist new file mode 100644 index 0000000..8969ce8 --- /dev/null +++ b/Examples/FireBaseUI/Info.plist @@ -0,0 +1,25 @@ + + + + + CFBundleIdentifier + org.compnerd.SwiftFirebase.FireBaseUI + ApplicationSceneManifest + + ApplicationSupportsMultipleScenes + + SceneConfigurations + + UIWindowSceneSessionRoleApplication + + + SceneConfigurationName + Default Configuration + SceneDelegateClassName + FireBaseUI.SceneDelegate + + + + + + diff --git a/Examples/FireBaseUI/Resources/google-services.json b/Examples/FireBaseUI/Resources/google-services.json new file mode 100644 index 0000000..5b4e374 --- /dev/null +++ b/Examples/FireBaseUI/Resources/google-services.json @@ -0,0 +1,20 @@ +{ + "project_info": { + "project_id": "test-do-not-use", + "firebase_url": "https://test-do-not-use.firebaseio.com", + "storage_bucket": "test-do-not-use.firebaseio.com" + }, + "client": [ + { + "api_key": [ + { "current_key": "000000000000000000000000000000000000000" } + ], + "client_info": { + "mobilesdk_app_id": "1:999999999999:ios:0000000000000000", + "android_client_info": { + "package_name": "com.firebaseio.test-do-not-use" + } + } + } + ] +} diff --git a/Examples/FireBaseUI/SceneDelegate.swift b/Examples/FireBaseUI/SceneDelegate.swift new file mode 100644 index 0000000..de154d9 --- /dev/null +++ b/Examples/FireBaseUI/SceneDelegate.swift @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import SwiftWin32 + +final class SceneDelegate: WindowSceneDelegate { + var window: Window? + + func scene(_ scene: Scene, willConnectTo session: SceneSession, + options: Scene.ConnectionOptions) { + guard let windowScene = scene as? WindowScene else { return } + + self.window = Window(windowScene: windowScene) + self.window?.rootViewController = FireBaseUIViewController() + self.window?.makeKeyAndVisible() + } +} diff --git a/Examples/FireBaseUI/SwiftWin32+Extensions.swift b/Examples/FireBaseUI/SwiftWin32+Extensions.swift new file mode 100644 index 0000000..7014400 --- /dev/null +++ b/Examples/FireBaseUI/SwiftWin32+Extensions.swift @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import SwiftWin32 + +extension Button { + internal convenience init(frame: Rect, title: String) { + self.init(frame: frame) + self.setTitle(title, forState: .normal) + } +} + +extension Label { + internal convenience init(frame: Rect, title: String) { + self.init(frame: frame) + self.text = title + } +} + +extension Switch { + internal convenience init(frame: Rect, title: String) { + self.init(frame: frame) + self.title = title + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc74ae9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2023, Saleem Abdulrasool + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..9610fd9 --- /dev/null +++ b/Package.swift @@ -0,0 +1,90 @@ +// swift-tools-version:5.9 + +import PackageDescription + +let SwiftFirebase = + Package(name: "SwiftFirebase", + platforms: [ + .macOS(.v13) + ], + products: [ + .library(name: "FirebaseCore", targets: ["FirebaseCore"]), + .library(name: "FirebaseAuth", targets: ["FirebaseAuth"]), + .executable(name: "FireBaseUI", targets: ["FireBaseUI"]), + ], + dependencies: [ + .package(url: "https://github.com/compnerd/swift-win32", branch: "main"), + ], + targets: [ + .target(name: "firebase", + publicHeadersPath: "include", + cSettings: [ + .unsafeFlags([ + "-iapinotes-modules", "Sources/firebase/include", + ]), + .headerSearchPath("../../third_party/firebase-development/usr/include"), + ], + cxxSettings: [ + .define("__swift__"), + .define("INTERNAL_EXPERIMENTAL"), + .define("_CRT_SECURE_NO_WARNINGS", + .when(platforms: [.windows])), + ]), + .target(name: "FirebaseCore", + dependencies: ["firebase"], + cxxSettings: [ + .define("INTERNAL_EXPERIMENTAL"), + .define("_CRT_SECURE_NO_WARNINGS", + .when(platforms: [.windows])), + .headerSearchPath("../../third_party/firebase-development/usr/include"), + ], + swiftSettings: [ + .interoperabilityMode(.Cxx), + ]), + .target(name: "FirebaseAuth", + dependencies: ["firebase", "FirebaseCore"], + cxxSettings: [ + .define("INTERNAL_EXPERIMENTAL"), + .define("_CRT_SECURE_NO_WARNINGS", + .when(platforms: [.windows])), + .headerSearchPath("../../third_party/firebase-development/usr/include"), + ], + swiftSettings: [ + .interoperabilityMode(.Cxx), + ]), + .executableTarget(name: "FireBaseUI", + dependencies: [ + "FirebaseCore", + "FirebaseAuth", + .product(name: "SwiftWin32", package: "swift-win32"), + ], + path: "Examples/FireBaseUI", + resources: [.copy("Resources")], + packageAccess: false, + cSettings: [ + .unsafeFlags([ + "-iapinotes-modules", "Sources/firebase/include", + ]), + ], + cxxSettings: [ + .define("INTERNAL_EXPERIMENTAL"), + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), + ], + swiftSettings: [ + .interoperabilityMode(.Cxx), + .unsafeFlags(["-parse-as-library"]) + ], + linkerSettings: [ + .unsafeFlags([ + "-Lthird_party/firebase-development/usr/libs/windows", + "-Lthird_party/firebase-development/usr/libs/windows/deps/app", + "-Lthird_party/firebase-development/usr/libs/windows/deps/app/external", + ]), + .linkedLibrary("crypto"), + .linkedLibrary("firebase_rest_lib"), + .linkedLibrary("flatbuffers"), + .linkedLibrary("libcurl"), + .linkedLibrary("ssl"), + .linkedLibrary("zlibstatic"), + ]) + ]) diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c958d6 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# SwiftFirebase + +Swift bindings for +[firebase-cpp-sdk](https://github.com/firebase/firebase-cpp-sdk), loosely +modelled after the iOS APIs. It serves as both exploration of the C++ Interop +as well as a means of using Firebase on Windows from Swift. + +### Requirements + +Requires that the Firebase C++ SDK is installed. You can use the latest binary +release or build it from source. + +The `google-services.json`` file must be updated with the proper values. +Additionally, the authentication information which is currently hardcoded into +the demo application must be updated. + +### Building + +The Swift support requires a couple of workarounds. The changes are available +in the patches directory. They should be applied to +[firebase-cpp-sdk](https://github.com/firebase/firebase-cpp-sdk) prior to +building. For convenience, a build of firebase is expected in the top level +under `third_party/firebase-development/usr`. diff --git a/Sources/FirebaseAuth/FIRActionCodeOperation.swift b/Sources/FirebaseAuth/FIRActionCodeOperation.swift new file mode 100644 index 0000000..5f3e3e6 --- /dev/null +++ b/Sources/FirebaseAuth/FIRActionCodeOperation.swift @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BSD-3-Clause + +public enum ActionCodeOperation: Int { + case FIRActionCodeOperationUnknown + case FIRActionCodeOperationPasswordReset + case FIRActionCodeOperationVerifyEmail + case FIRActionCodeOperationRecoverEmail + case FIRActionCodeOperationEmailLink + case FIRActionCodeOperationVerifyAndChangeEmail + case FIRActionCodeOperationRevertSecondFactorAddition +} diff --git a/Sources/FirebaseAuth/FIRAuthTokenResult.swift b/Sources/FirebaseAuth/FIRAuthTokenResult.swift new file mode 100644 index 0000000..6435fbd --- /dev/null +++ b/Sources/FirebaseAuth/FIRAuthTokenResult.swift @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase + +import Foundation + +public struct AuthTokenResult { + public let token: String + + public let expirationDate: Date + + public let authDate: Date + + public let issuedAtDate: Date + + public let signInProvider: String + + public let signInSecondFactor: String + + public let claims: [String:Any] + + internal init(_ token: String) throws { + let components = token.components(separatedBy: ".") + // The JWT should have three components + guard components.count == 3 else { + throw NSError(domain: "com.google.firebase.auth", + code: AuthError.Code.malformedJWT.rawValue, + userInfo: [NSLocalizedDescriptionKey:"Failed to decode token"]) + } + + var payload = components[1].replacing("_", with: "/") + .replacing("-", with: "+") + // Pad to 4 character alignment for base64 decoding + if payload.count % 4 != 0 { + payload.append(String(repeating: "=", count: 4 - payload.count % 4)) + } + + guard let data = Data(base64Encoded: payload, + options: .ignoreUnknownCharacters) else { + throw NSError(domain: "com.google.firebase.auth", + code: AuthError.Code.malformedJWT.rawValue, + userInfo: [NSLocalizedDescriptionKey:"Failed to decode token payload"]) + } + + let options: JSONSerialization.ReadingOptions = + [.mutableContainers, .allowFragments] + guard let contents = + try? JSONSerialization.jsonObject(with: data, options: options) + as? [String:Any] else { + throw NSError(domain: "com.google.firebase.auth", + code: AuthError.Code.malformedJWT.rawValue, + userInfo: [NSLocalizedDescriptionKey:"Failed to deserialise token payload"]) + } + + // These are dates since 00:00:00 January 1 1970, as described by the + // Terminology section in the JWT spec. + // https://tools.ietf.org/html/rfc7519 + guard let authDate = contents["auth_time"] as? TimeInterval, + let expirationDate = contents["exp"] as? TimeInterval, + let issueDate = contents["iat"] as? TimeInterval else { + throw NSError(domain: "com.google.firebase.auth", + code: AuthError.Code.malformedJWT.rawValue, + userInfo: [NSLocalizedDescriptionKey:"Missing fields in token payload"]) + } + + self.token = token + self.expirationDate = Date(timeIntervalSince1970: expirationDate) + self.authDate = Date(timeIntervalSince1970: authDate) + self.issuedAtDate = Date(timeIntervalSince1970: issueDate) + self.signInProvider = contents["sign_in_provider"] as? String ?? "" + self.signInSecondFactor = contents["sign_in_second_factor"] as? String ?? "" + self.claims = contents + } +} diff --git a/Sources/FirebaseAuth/FirebaseAuth+Swift.swift b/Sources/FirebaseAuth/FirebaseAuth+Swift.swift new file mode 100644 index 0000000..01c42cc --- /dev/null +++ b/Sources/FirebaseAuth/FirebaseAuth+Swift.swift @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase +import FirebaseCore + +import CxxShim +import Foundation + +public typealias Auth = UnsafeMutablePointer + +@available(*, unavailable) +public enum AuthAPNSTokenType: Int { + case FIRAuthAPNSTokenTypeUnknown + case FIRAuthAPNSTokenTypeSandbox + case FIRAuthAPNSTokenTypeProd +} + +extension Auth { + public var app: FirebaseApp? { + self.pointee.__appUnsafe() + } + + public var currentUser: User? { + let user = self.pointee.current_user() + guard user.is_valid() else { return nil } + return user + } + + public var languageCode: String? { + get { + let code = String(self.pointee.language_code()) + guard !code.isEmpty else { return nil } + return String(code) + } + set { self.pointee.set_language_code(newValue) } + } + + // @available(*, unavailable) + // public var settings: AuthSettings? { get set } + + // @available(*, unavailable) + // public var userAccessGroup: String? { get } + + // @available(*, unavailable) + // public var shareAuthStateAcrossDevices: Bool { get set } + + // @available(*, unavailable) + // public var tennantID: String? { get set } + + // @available(*, unavailable) + // public var apnsToken: Data? { get set } + + public static func auth() -> Auth { + // TODO(compnerd) convert this to an exception + guard let application = firebase.App.GetInstance() else { + fatalError("no default application") + } + return auth(app: application) + } + + public static func auth(app: FirebaseApp) -> Auth { + firebase.auth.Auth.GetAuth(app, nil) + } + + public func updateCurrentUser(_ user: User) async throws { + fatalError("\(#function) not yet implemented") + } + + public func fetchSignInMethods(forEmail email: String) async throws + -> [String] { + typealias Promise = CheckedContinuation, any Error> + let providers = try await withCheckedThrowingContinuation { (continuation: Promise) in + let future = self.pointee.FetchProvidersForEmail(email) + withUnsafePointer(to: continuation) { continuation in + future.OnCompletion_SwiftWorkaround({ future, pvContinuation in + let pContinuation = pvContinuation?.assumingMemoryBound(to: Promise.self) + if future.pointee.error() == 0 { + pContinuation.pointee.resume(returning: .init(mutating: future.pointee.__resultUnsafe())) + } else { + let code = future.pointee.error() + let message = String(cString: future.pointee.__error_messageUnsafe()!) + pContinuation.pointee.resume(throwing: FirebaseError(code: code, message: message)) + } + }, UnsafeMutableRawPointer(mutating: continuation)) + } + future.Wait(firebase.FutureBase.kWaitTimeoutInfinite) + } + return providers.pointee.providers.compactMap(String.init) + } + + public func signIn(withEmail email: String, password: String) async throws + -> AuthDataResult { + typealias Promise = CheckedContinuation + return try await withCheckedThrowingContinuation { (continuation: Promise) in + let future = self.pointee.SignInWithEmailAndPassword(email, password) + withUnsafePointer(to: continuation) { continuation in + future.OnCompletion_SwiftWorkaround({ future, pvContinuation in + let pContinuation = pvContinuation?.assumingMemoryBound(to: Promise.self) + if future.pointee.error() == 0 { + pContinuation.pointee.resume(returning: .init(mutating: future.pointee.__resultUnsafe())) + } else { + let code = future.pointee.error() + let message = String(cString: future.pointee.__error_messageUnsafe()!) + pContinuation.pointee.resume(throwing: FirebaseError(code: code, message: message)) + } + }, UnsafeMutableRawPointer(mutating: continuation)) + } + future.Wait(firebase.FutureBase.kWaitTimeoutInfinite) + } + } + + public func signIn(withEmail email: String, link: String) async throws + -> AuthDataResult { + fatalError("\(#function) not yet implemented") + } + + // signInWithProvider:UIDelegate:completion: + + // public func signIn(with credential: AuthCredential) async throws + // -> AuthDataResult { + // fatalError("\(#function) not yet implemented") + // } + + public func signInAnonymously() async throws -> AuthDataResult { + fatalError("\(#function) not yet implemented") + } + + public func signIn(withCustomToken token: String) async throws + -> AuthDataResult { + fatalError("\(#function) not yet implemented") + } + + public func createUser(withEmail email: String, password: String) async throws + -> AuthDataResult { + typealias Promise = CheckedContinuation + return try await withCheckedThrowingContinuation { (continuation: Promise) in + let future = self.pointee.CreateUserWithEmailAndPassword(email, password) + withUnsafePointer(to: continuation) { continuation in + future.OnCompletion_SwiftWorkaround({ future, pvContinuation in + let pContinuation = pvContinuation?.assumingMemoryBound(to: Promise.self) + if future.pointee.error() == 0 { + pContinuation.pointee.resume(returning: .init(mutating: future.pointee.__resultUnsafe())) + } else { + let code = future.pointee.error() + let message = String(cString: future.pointee.__error_messageUnsafe()!) + pContinuation.pointee.resume(throwing: FirebaseError(code: code, message: message)) + } + }, UnsafeMutableRawPointer(mutating: continuation)) + } + future.Wait(firebase.FutureBase.kWaitTimeoutInfinite) + } + } + + public func confirmPasswordReset(withCode code: String, + newPassword: String) async throws { + fatalError("\(#function) not yet implemented") + } + + // public func checkActionCode(_ code: String) async throws -> ActionCodeInfo { + // fatalError("\(#function) not yet implemented") + // } + + public func verifyPasswordResetCode(_ code: String) async throws -> String { + fatalError("\(#function) not yet implemented") + } + + public func applyActionCode(_ code: String) async throws { + fatalError("\(#function) not yet implemented") + } + + public func sendPasswordReset(withEmail email: String) async throws { + typealias Promise = CheckedContinuation + return try await withCheckedThrowingContinuation { (continuation: Promise) in + let future = self.pointee.SendPasswordResetEmail(email) + withUnsafePointer(to: continuation) { continuation in + future.OnCompletion_SwiftWorkaround({ future, pvContinuation in + let pContinuation = pvContinuation?.assumingMemoryBound(to: Promise.self) + if future.pointee.error() == 0 { + pContinuation.pointee.resume() + } else { + let code = future.pointee.error() + let message = String(cString: future.pointee.__error_messageUnsafe()!) + pContinuation.pointee.resume(throwing: FirebaseError(code: code, message: message)) + } + }, UnsafeMutableRawPointer(mutating: continuation)) + } + future.Wait(firebase.FutureBase.kWaitTimeoutInfinite) + } + } + + // public func sendPasswordReset(withEmail email: String, + // actionCodeSettings: ActionCodeSettings) async throws { + // fatalError("\(#function) not yet implemented") + // } + + // public func sendSignInLink(toEmail email: String, + // actionCodeSettings: ActionCodeSettings) async throws { + // fatalError("\(#function) not yet implemented") + // } + + public func signOut() throws { + self.pointee.SignOut() + } + + public func isSignIn(withEmailLink link: String) -> Bool { + fatalError("\(#function) not yet implemented") + } + + // public func addStateDidChangeListener(_ listener: @escaping (Auth, User?) -> Void) + // -> AuthStateDidChangeListenerHandle { + // fatalError("\(#function) not yet implemented") + // } + + // public func removeStateDidChangeListener(_ listenerHandle: AuthStateDidChangeListenerHandle) { + // fatalError("\(#function) not yet implemented") + // } + + // public func addIDTokenDidChangeListener(_ listener: @escaping (Auth, User?) -> Void) + // -> IDTokenDidChangeListenerHandle { + // fatalError("\(#function) not yet implemented") + // } + + // public func removeIDTokenDidChangeListener(_ listenerHandle: IDTokenDidChangeListenerHandle) { + // fatalError("\(#function) not yet implemented") + // } + + public func useAppLangauge() { + fatalError("\(#function) not yet implemented") + } + + public func useEmulator(withHost host: String, port: Int) { + fatalError("\(#function) not yet implemented") + } + + public func canHandle(_ url: URL) -> Bool { + fatalError("\(#function) not yet implemented") + } + + @available(*, unavailable) + public func setAPNSToken(_ token: Data, type: AuthAPNSTokenType) { + } + + public func canHandleNotification(_ userInfo: [AnyHashable:Any]) -> Bool { + fatalError("\(#function) not yet implemented") + } + + public func revokeToken(withAuthorizationCode authorizationCode: String) async throws { + fatalError("\(#function) not yet implemented") + } + + public func useUserAccessGroup(_ accessGroup: String?) throws { + fatalError("\(#function) not yet implemented") + } + + public func getStoredUser(forAccessGroup accessGroup: String?) throws -> User? { + fatalError("\(#function) not yet implemented") + } +} diff --git a/Sources/FirebaseAuth/FirebaseAuthError.swift b/Sources/FirebaseAuth/FirebaseAuthError.swift new file mode 100644 index 0000000..8979224 --- /dev/null +++ b/Sources/FirebaseAuth/FirebaseAuthError.swift @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import Foundation + +public struct AuthError { +} + +extension AuthError: Error { +} + +extension AuthError { + public enum Code: Int { + case invalidCustomToken = 17000 + case customTokenMismatch = 17002 + case invalidCredential = 17004 + case userDisabled = 17005 + case operationNotAllowed = 17006 + case emailAlreadyInUse = 17007 + case invalidEmail = 17008 + case wrongPassword = 17009 + case tooManyRequests = 17010 + case userNotFound = 17011 + case accountExistsWithDifferentCredential = 17012 + case requiresRecentLogin = 17014 + case providerAlreadyLinked = 17015 + case noSuchProvider = 17016 + case invalidUserToken = 17017 + case networkError = 17020 + case userTokenExpired = 17021 + case invalidAPIKey = 17023 + case userMismatch = 17024 + case credentialAlreadyInUse = 17025 + case weakPassword = 17026 + case appNotAuthorized = 17028 + case expiredActionCode = 17029 + case invalidActionCode = 17030 + case invalidMessagePayload = 17031 + case invalidSender = 17032 + case invalidRecipientEmail = 17033 + case missingEmail = 17034 + case missingIosBundleID = 17036 + case missingAndroidPackageName = 17037 + case unauthorizedDomain = 17038 + case invalidContinueURI = 17039 + case missingContinueURI = 17040 + case missingPhoneNumber = 17041 + case invalidPhoneNumber = 17042 + case missingVerificationCode = 17043 + case invalidVerificationCode = 17044 + case missingVerificationID = 17045 + case invalidVerificationID = 17046 + case missingAppCredential = 17047 + case invalidAppCredential = 17048 + case sessionExpired = 17051 + case quotaExceeded = 17052 + case missingAppToken = 17053 + case notificationNotForwarded = 17054 + case appNotVerified = 17055 + case captchaCheckFailed = 17056 + case webContextAlreadyPresented = 17057 + case webContextCancelled = 17058 + case appVerificationUserInteractionFailure = 17059 + case invalidClientID = 17060 + case webNetworkRequestFailed = 17061 + case webInternalError = 17062 + case webSignInUserInteractionFailure = 17063 + case localPlayerNotAuthenticated = 17066 + case nullUser = 17067 + case dynamicLinkNotActivated = 17068 + case invalidProviderID = 17071 + case tenantIDMismatch = 17072 + case unsupportedTenantOperation = 17073 + case invalidDynamicLinkDomain = 17074 + case rejectedCredential = 17075 + case gameKitNotLinked = 17076 + case secondFactorRequired = 17078 + case missingMultiFactorSession = 17081 + case missingMultiFactorInfo = 17082 + case invalidMultiFactorSession = 17083 + case multiFactorInfoNotFound = 17084 + case adminRestrictedOperation = 17085 + case unverifiedEmail = 17086 + case secondFactorAlreadyEnrolled = 17087 + case maximumSecondFactorCountExceeded = 17088 + case unsupportedFirstFactor = 17089 + case emailChangeNeedsVerification = 17090 + case missingOrInvalidNonce = 17094 + case blockingCloudFunctionError = 17105 + case missingClientIdentifier = 17993 + case keychainError = 17995 + case internalError = 17999 + case malformedJWT = 18000 + } +} diff --git a/Sources/FirebaseAuth/FirebaseAuthResult+Swift.swift b/Sources/FirebaseAuth/FirebaseAuthResult+Swift.swift new file mode 100644 index 0000000..aa8f1c4 --- /dev/null +++ b/Sources/FirebaseAuth/FirebaseAuthResult+Swift.swift @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase + +public typealias AuthDataResult = UnsafeMutablePointer + +extension AuthDataResult { + public var user: User? { + let user = self.pointee.user + guard user.is_valid() else { return nil } + return user + } + + // public var additionalUserInfo: AdditionalUserInfo? { + // fatalError("\(#function) not yet implemented") + // } + + // public var credential: AuthCredential? { + // fatalError("\(#function) not yet implemented") + // } +} diff --git a/Sources/FirebaseAuth/FirebaseUser+Swift.swift b/Sources/FirebaseAuth/FirebaseUser+Swift.swift new file mode 100644 index 0000000..c0a9c9e --- /dev/null +++ b/Sources/FirebaseAuth/FirebaseUser+Swift.swift @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import CxxStdlib +import Foundation + +import firebase +import FirebaseCore + +public typealias User = firebase.auth.User + +public protocol UserInfo { + var providerID: String { get } + var uid: String { get } + var displayName: String? { get } + var photoURL: URL? { get } + var email: String? { get } + var phoneNumber: String? { get } +} + +extension User { + public var isAnonymous: Bool { + self.is_anonymous() + } + + public var isEmailVerified: Bool { + self.is_email_verified() + } + + public var refreshToken: String? { + fatalError("\(#function) not yet implemented") + } + + // public var providerData: [UserInfo] { + // fatalError("\(#function) not yet implemented") + // } + + // public var metadata: UserMetadata { + // fatalError("\(#function) not yet implemented") + // } + + public var tenantID: String? { + fatalError("\(#function) not yet implemented") + } + + // public var multiFactor: MultiFactor { + // fatalError("\(#function) not yet implemented") + // } + + public func updateEmail(to email: String) async throws { + fatalError("\(#function) not yet implemented") + } + + public func updatePassword(to password: String) async throws { + fatalError("\(#function) not yet implemented") + } + + // public func updatePhoneNumber(_ credential: PhoneAuthCredential) async throws { + // fatalError("\(#function) not yet implemented") + // } + + // public func createProfileChangeRequest() -> UserProfileChangeRequest { + // fatalError("\(#function) not yet implemented") + // } + + public mutating func reload() async throws { + typealias Promise = CheckedContinuation + try await withCheckedThrowingContinuation { (continuation: Promise) in + let future = self.Reload() + withUnsafePointer(to: continuation) { continuation in + future.OnCompletion_SwiftWorkaround({ future, pvContinuation in + let pContinuation = pvContinuation?.assumingMemoryBound(to: Promise.self) + if future.pointee.error() == 0 { + pContinuation.pointee.resume() + } else { + let code = future.pointee.error() + let message = String(cString: future.pointee.__error_messageUnsafe()!) + pContinuation.pointee.resume(throwing: FirebaseError(code: code, message: message)) + } + }, UnsafeMutableRawPointer(mutating: continuation)) + } + future.Wait(firebase.FutureBase.kWaitTimeoutInfinite) + } + } + + // public mutating func reauthenticate(with credential: AuthCredential) async throws + // -> AuthResult { + // fatalError("\(#function) not yet implemented") + // } + + // -reauthenticateWithProvider:UIDelegate:completion: + + public mutating func getIDTokenResult() async throws -> AuthTokenResult { + return try await getIDTokenResult(forcingRefresh: false) + } + + public mutating func getIDTokenResult(forcingRefresh forceRefresh: Bool) async throws + -> AuthTokenResult { + return try await AuthTokenResult(idTokenForcingRefresh(forceRefresh)) + } + + public mutating func getIDToken() async throws -> String { + return try await idTokenForcingRefresh(false) + } + + public mutating func idTokenForcingRefresh(_ forceRefresh: Bool) async throws + -> String { + typealias Promise = CheckedContinuation + return try await withCheckedThrowingContinuation { (continuation: Promise) in + let future = self.GetToken(forceRefresh) + withUnsafePointer(to: continuation) { continuation in + future.OnCompletion_SwiftWorkaround({ future, pvContinuation in + let pContinuation = pvContinuation?.assumingMemoryBound(to: Promise.self) + if future.pointee.error() == 0 { + pContinuation.pointee.resume(returning: String(future.pointee.__resultUnsafe().pointee)) + } else { + let code = future.pointee.error() + let message = String(cString: future.pointee.__error_messageUnsafe()!) + pContinuation.pointee.resume(throwing: FirebaseError(code: code, message: message)) + } + }, UnsafeMutableRawPointer(mutating: continuation)) + } + future.Wait(firebase.FutureBase.kWaitTimeoutInfinite) + } + } + + // public func link(with credential: AuthCredential) async throws + // -> AuthDataResult { + // fatalError("\(#function) not yet implemented") + // } + + // -linkWithProvider:UIDelegate:completion + + public func unlink(from provider: String) async throws -> User { + fatalError("\(#function) not yet implemented") + } + + public mutating func sendEmailVerification() async throws { + typealias Promise = CheckedContinuation + try await withCheckedThrowingContinuation { (continuation: Promise) in + let future = self.SendEmailVerification() + withUnsafePointer(to: continuation) { continuation in + future.OnCompletion_SwiftWorkaround({ future, pvContinuation in + let pContinuation = pvContinuation?.assumingMemoryBound(to: Promise.self) + if future.pointee.error() == 0 { + pContinuation.pointee.resume() + } else { + let code = future.pointee.error() + let message = String(cString: future.pointee.__error_messageUnsafe()!) + pContinuation.pointee.resume(throwing: FirebaseError(code: code, message: message)) + } + }, UnsafeMutableRawPointer(mutating: continuation)) + } + future.Wait(firebase.FutureBase.kWaitTimeoutInfinite) + } + } + + // public func sendEmailVerification(with actionCodeSettings: ActionCodeSettings) async throws { + // fatalError("\(#function) not yet implemented") + // } + + public func delete() async throws { + fatalError("\(#function) not yet implemented") + } + + public func sendEmailVerification(beforeUpdatingEmail email: String) async throws { + fatalError("\(#function) not yet implemented") + } + + // public func sendEmailVerification(beforeUpdatingEmail email: String, + // actionCodeSettings: ActionCodeSettings) async throws { + // fatalError("\(#function) not yet implemented") + // } +} + +extension User: UserInfo { + public var providerID: String { + String(swift_firebase.swift_cxx_shims.firebase.auth.user_provider_id(self)) + } + + public var uid: String { + String(swift_firebase.swift_cxx_shims.firebase.auth.user_uid(self)) + } + + public var displayName: String? { + let name = String(swift_firebase.swift_cxx_shims.firebase.auth.user_display_name(self)) + return name.isEmpty ? nil : name + } + + public var photoURL: URL? { + let url = String(swift_firebase.swift_cxx_shims.firebase.auth.user_photo_url(self)) + return url.isEmpty ? nil : URL(string: url) + } + + public var email: String? { + let email = String(swift_firebase.swift_cxx_shims.firebase.auth.user_email(self)) + return email.isEmpty ? nil : email + } + + public var phoneNumber: String? { + let number = String(swift_firebase.swift_cxx_shims.firebase.auth.user_phone_number(self)) + return number.isEmpty ? nil : number + } +} diff --git a/Sources/FirebaseCore/FirebaseApp+Swift.swift b/Sources/FirebaseCore/FirebaseApp+Swift.swift new file mode 100644 index 0000000..1c6fea8 --- /dev/null +++ b/Sources/FirebaseCore/FirebaseApp+Swift.swift @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase + +public typealias FirebaseApp = UnsafeMutablePointer + +extension FirebaseApp { + public static func configure() { + _ = firebase.App.Create() + } + + public static func configure(options: FirebaseOptions) { + _ = firebase.App.Create(options.pointee) + } + + public static func configure(name: String, options: FirebaseOptions) { + _ = firebase.App.Create(options.pointee, name) + } + + public static func app() -> FirebaseApp? { + firebase.App.GetInstance() + } + + public static func app(name: String) -> FirebaseApp? { + firebase.App.GetInstance(name) + } + + public static var allApps: [String:FirebaseApp]? { + let applications = firebase.App.GetApps() + guard !applications.isEmpty else { return nil } + return .init(uniqueKeysWithValues: applications.compactMap { + guard let application = $0 else { return nil } + return (application.name, application) + }) + } + + public func delete() async -> Bool { + fatalError("\(#function) not yet implemented") + } + + public var name: String { + String(cString: self.pointee.__nameUnsafe()!) + } + + public var options: UnsafePointer { + // TODO(compnerd) ensure that the `FirebaseOptions` API is applied to this. + self.pointee.__optionsUnsafe() + } + + public var isDataCollectionDefaultEnabled: Bool { + get { self.pointee.IsDataCollectionDefaultEnabled() } + set { self.pointee.SetDataCollectionDefaultEnabled(newValue) } + } +} diff --git a/Sources/FirebaseCore/FirebaseConfiguration.swift b/Sources/FirebaseCore/FirebaseConfiguration.swift new file mode 100644 index 0000000..afced5c --- /dev/null +++ b/Sources/FirebaseCore/FirebaseConfiguration.swift @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase + +public class FirebaseConfiguration { + public static let shared: FirebaseConfiguration = FirebaseConfiguration() + + public func setLoggerLevel(_ loggerLevel: FirebaseLoggerLevel) { + firebase.SetLogLevel(loggerLevel) + } +} diff --git a/Sources/FirebaseCore/FirebaseError.swift b/Sources/FirebaseCore/FirebaseError.swift new file mode 100644 index 0000000..d759c9e --- /dev/null +++ b/Sources/FirebaseCore/FirebaseError.swift @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BSD-3-Clause + +public struct FirebaseError: Error { + public let code: CInt + public let message: String + + package init(code: CInt, message: String) { + self.code = code + self.message = message + } +} diff --git a/Sources/FirebaseCore/FirebaseLogging+Swift.swift b/Sources/FirebaseCore/FirebaseLogging+Swift.swift new file mode 100644 index 0000000..f0aabac --- /dev/null +++ b/Sources/FirebaseCore/FirebaseLogging+Swift.swift @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase + +public typealias FirebaseLoggerLevel = firebase.LogLevel + +// Workaround #67604 - Unable to import C++ enumerations +extension firebase.LogLevel { + public static var kLogLevelVerbose: Self { .init(0) } + public static var kLogLevelDebug: Self { .init(1) } + public static var kLogLevelInfo: Self { .init(2) } + public static var kLogLevelWarning: Self { .init(3) } + public static var kLogLevelError: Self { .init(4) } + public static var kLogLevelAssert: Self { .init(5) } +} + +extension firebase.LogLevel { + public static var verbose: Self { kLogLevelVerbose } + public static var debug: Self { kLogLevelDebug } + public static var info: Self { kLogLevelInfo } + public static var warning: Self { kLogLevelWarning } + public static var error: Self { kLogLevelError } + public static var assert: Self { kLogLevelAssert } +} + +extension firebase.LogLevel { + public static var min: Self { .error } + public static var max: Self { .debug } +} + +extension firebase.LogLevel: CustomStringConvertible { + public var description: String { + switch self { + case .verbose: return "Verbose" + case .debug: return "Debug" + case .info: return "Info" + case .warning: return "Warning" + case .error: return "Error" + case .assert: return "Assert" + default: fatalError("unknown value for firebase.LogLevel") + } + } +} diff --git a/Sources/FirebaseCore/FirebaseOptions+Swift.swift b/Sources/FirebaseCore/FirebaseOptions+Swift.swift new file mode 100644 index 0000000..549422e --- /dev/null +++ b/Sources/FirebaseCore/FirebaseOptions+Swift.swift @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: BSD-3-Clause + +import firebase + +import Foundation + +public typealias FirebaseOptions = UnsafeMutablePointer + +extension firebase.AppOptions: CustomDebugStringConvertible { + public var debugDescription: String { + """ + AppOptions { + package_name_: "\(String(cString: self.__package_nameUnsafe()))" + api_key_: "\(String(cString: self.__api_keyUnsafe()))" + app_id_: "\(String(cString: self.__app_idUnsafe()))" + client_id_: "\(String(cString: self.__client_idUnsafe()))" + database_url_: "\(String(cString: self.__database_urlUnsafe()))" + ga_tracking_id_: "\(String(cString: self.__ga_tracking_idUnsafe()))" + fcm_sender_id_: "\(String(cString: self.__messaging_sender_idUnsafe()))" + storage_bucket_: "\(String(cString: self.__storage_bucketUnsafe()))" + project_id_: "\(String(cString: self.__project_idUnsafe()))" + } + """ + } +} + +extension FirebaseOptions { + public static func defaultOptions() -> FirebaseOptions? { + firebase.AppOptions.LoadDefault(nil) + } + + public init?(contentsOfFile plistPath: String) { + fatalError("\(#function) NYI") + return nil + } + + public init(googleAppID: String, gcmSenderID GCMSenderID: String) { + self = .allocate(capacity: 1) + self.initialize(to: firebase.AppOptions()) + self.googleAppID = googleAppID + self.gcmSenderID = GCMSenderID + } + + public var apiKey: String? { + get { + guard let value = self.pointee.__api_keyUnsafe() else { return nil } + return String(cString: value) + } + set { self.pointee.set_api_key(newValue) } + } + + public var bundleID: String { + get { + guard let value = self.pointee.__package_nameUnsafe() else { + return Bundle.main.bundleIdentifier! + } + return String(cString: value) + } + set { self.pointee.set_package_name(newValue) } + } + + public var clientID: String? { + get { + guard let value = self.pointee.__client_idUnsafe() else { return nil } + return String(cString: value) + } + set { self.pointee.set_client_id(newValue) } + } + + public var trackingID: String? { + get { + guard let value = self.pointee.__ga_tracking_idUnsafe() else { + return nil + } + return String(cString: value) + } + set { self.pointee.set_ga_tracking_id(newValue) } + } + + public var gcmSenderID: String { + get { String(cString: self.pointee.__messaging_sender_idUnsafe()!) } + set { self.pointee.set_messaging_sender_id(newValue) } + } + + public var projectID: String? { + get { + guard let value = self.pointee.__project_idUnsafe() else { return nil } + return String(cString: value) + } + set { self.pointee.set_project_id(newValue) } + } + + @available(*, unavailable) + public var androidClientID: String? { nil } + + public var googleAppID: String? { + get { + guard let value = self.pointee.__app_idUnsafe() else { return nil } + return String(cString: value) + } + set { self.pointee.set_app_id(newValue) } + } + + public var databaseURL: String? { + get { + guard let value = self.pointee.__database_urlUnsafe() else { return nil } + return String(cString: value) + } + set { self.pointee.set_database_url(newValue) } + } + + @available(*, unavailable) + public var deepLinkURLScheme: String? { nil } + + public var storageBucket: String? { + get { + guard let value = self.pointee.__storage_bucketUnsafe() else { + return nil + } + return String(cString: value) + } + set { self.pointee.set_storage_bucket(newValue) } + } + + @available(*, unavailable) + public var appGroupId: String? { nil } +} diff --git a/Sources/firebase/include/FirebaseApp.hh b/Sources/firebase/include/FirebaseApp.hh new file mode 100644 index 0000000..59e79c6 --- /dev/null +++ b/Sources/firebase/include/FirebaseApp.hh @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef firebase_include_FirebaseApp_hh +#define firebase_include_FirebaseApp_hh + +#include + +#endif diff --git a/Sources/firebase/include/FirebaseAuth.hh b/Sources/firebase/include/FirebaseAuth.hh new file mode 100644 index 0000000..cfaab77 --- /dev/null +++ b/Sources/firebase/include/FirebaseAuth.hh @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef firebase_include_FirebaseAuth_hh +#define firebase_include_FirebaseAuth_hh + +#include + +namespace swift_firebase::swift_cxx_shims::firebase::auth { +inline std::string +user_display_name(const ::firebase::auth::User &user) noexcept { + return user.display_name(); +} + +inline std::string user_email(const ::firebase::auth::User &user) noexcept { + return user.email(); +} + +inline std::string +user_phone_number(const ::firebase::auth::User &user) noexcept { + return user.phone_number(); +} + +inline std::string user_photo_url(const ::firebase::auth::User &user) noexcept { + return user.photo_url(); +} + +inline std::string +user_provider_id(const ::firebase::auth::User &user) noexcept { + return user.provider_id(); +} + +inline std::string +user_uid(const ::firebase::auth::User &user) noexcept { + return user.uid(); +} +} + +#endif + diff --git a/Sources/firebase/include/FirebaseCore.hh b/Sources/firebase/include/FirebaseCore.hh new file mode 100644 index 0000000..188d19b --- /dev/null +++ b/Sources/firebase/include/FirebaseCore.hh @@ -0,0 +1,7 @@ + +#ifndef firebase_include_FirebaseCore_hh +#define firebase_include_FirebaseCore_hh + +#include + +#endif diff --git a/Sources/firebase/include/FirebaseLogging.hh b/Sources/firebase/include/FirebaseLogging.hh new file mode 100644 index 0000000..a47e81b --- /dev/null +++ b/Sources/firebase/include/FirebaseLogging.hh @@ -0,0 +1,7 @@ + +#ifndef firebase_include_FirebaseLogging_hh +#define firebase_include_FirebaseLogging_hh + +#include + +#endif diff --git a/Sources/firebase/include/firebase.apinotes b/Sources/firebase/include/firebase.apinotes new file mode 100644 index 0000000..53d8675 --- /dev/null +++ b/Sources/firebase/include/firebase.apinotes @@ -0,0 +1,15 @@ +Name: firebase +Enumerators: + - Name: kLogLevelVerbose + SwiftName: verbose + - Name: kLogLevelDebug + SwiftName: debug + - Name: kLogLevelInfo + SwiftName: info + - Name: kLogLevelWarning + SwiftName: warning + - Name: kLogLevelError + SwiftName: error + - Name: kLogLevelAssert + SwiftName: assert + diff --git a/Sources/firebase/include/module.modulemap b/Sources/firebase/include/module.modulemap new file mode 100644 index 0000000..abd2c29 --- /dev/null +++ b/Sources/firebase/include/module.modulemap @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD-3-Clause + +module firebase [system] { + requires cplusplus + + module app { + header "FirebaseApp.hh" + export * + link "firebase_app" + } + + module auth { + header "FirebaseAuth.hh" + export * + link "firebase_auth" + } + + module core { + header "FirebaseCore.hh" + export * + } + + module logging { + header "FirebaseLogging.hh" + export * + } +} diff --git a/Sources/firebase/shims.cc b/Sources/firebase/shims.cc new file mode 100644 index 0000000..d7a9f26 --- /dev/null +++ b/Sources/firebase/shims.cc @@ -0,0 +1,10 @@ + +#include + +#if defined(_WIN32) +namespace firebase { +namespace auth { +Auth::Auth(const Auth &) noexcept = default; +} +} +#endif diff --git a/patches/0001-Add-a-couple-of-workarounds-for-Swift-on-Windows.patch b/patches/0001-Add-a-couple-of-workarounds-for-Swift-on-Windows.patch new file mode 100644 index 0000000..f07a8cb --- /dev/null +++ b/patches/0001-Add-a-couple-of-workarounds-for-Swift-on-Windows.patch @@ -0,0 +1,126 @@ +From 140b2a4d33c50deca8a1ab9284f1897c07a1ce52 Mon Sep 17 00:00:00 2001 +From: Saleem Abdulrasool +Date: Thu, 3 Aug 2023 21:40:01 -0700 +Subject: [PATCH] Add a couple of workarounds for Swift on Windows + +The C++ Interop efforts in Swift currently have some limitations. In +particular, it cannot support trivial types with non-trivial +destructors. As a workaround, provide a copy constructor which can be +used by the Swift interop while using the regular semantics for all +other cases. + +A second issue arises in the handling of futures. Unfortunately, it is +not currently possible to pass an indirect block parameter which +prevents the construction of a callback. Workaround this by providing +an inline shim to use a direct parameter (i.e. indirect value through a +pointer) which then allows a callback to be formed. + +Both of these items are being tracked upstream but seem to be +potentially sufficient to enable the use of Swift for using the C++ SDK +for desktop scenarios. +--- + app/src/include/firebase/future.h | 15 +++++++++++++++ + auth/CMakeLists.txt | 1 + + auth/src/auth_swift.cc | 26 ++++++++++++++++++++++++++ + auth/src/include/firebase/auth.h | 7 +++++++ + 4 files changed, 49 insertions(+) + create mode 100644 auth/src/auth_swift.cc + +diff --git a/app/src/include/firebase/future.h b/app/src/include/firebase/future.h +index 0d09fc07..9e325e3d 100644 +--- a/app/src/include/firebase/future.h ++++ b/app/src/include/firebase/future.h +@@ -407,6 +407,11 @@ class Future : public FutureBase { + /// when you set up the callback. + typedef void (*TypedCompletionCallback)(const Future& result_data, + void* user_data); ++#if defined(__swift__) ++ // TODO(apple/swift#67662) indirect block parameters are unsupported ++ typedef void (*TypedCompletionCallback_SwiftWorkaround)(const Future* result_data, ++ void* user_data); ++#endif + + /// Construct a future. + Future() {} +@@ -464,6 +469,16 @@ class Future : public FutureBase { + inline void OnCompletion(TypedCompletionCallback callback, + void* user_data) const; + ++#if defined(__swift__) ++ // TODO(apple/swift#67662) indirect block parameters are unsupported ++ inline void OnCompletion_SwiftWorkaround(TypedCompletionCallback_SwiftWorkaround callback, ++ void *user_data) const { ++ OnCompletion([callback, user_data](const Future& future) { ++ callback(&future, user_data); ++ }); ++ } ++#endif ++ + #if defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN) + /// Register a single callback that will be called at most once, when the + /// future is completed. +diff --git a/auth/CMakeLists.txt b/auth/CMakeLists.txt +index 2dc7c4e2..8ee2a964 100644 +--- a/auth/CMakeLists.txt ++++ b/auth/CMakeLists.txt +@@ -51,6 +51,7 @@ build_flatbuffers("${flatbuffer_schemas}" + # Common source files used by all platforms + set(common_SRCS + src/auth.cc ++ src/auth_swift.cc + src/credential.cc + src/common.cc + src/common.h +diff --git a/auth/src/auth_swift.cc b/auth/src/auth_swift.cc +new file mode 100644 +index 00000000..63248c18 +--- /dev/null ++++ b/auth/src/auth_swift.cc +@@ -0,0 +1,26 @@ ++/* ++ * Copyright 2016 Google LLC ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++#define __swift__ ++#include "auth/src/include/firebase/auth.h" ++ ++#if FIREBASE_PLATFORM_WINDOWS ++namespace firebase { ++namespace auth { ++Auth::Auth(const Auth &) noexcept = default; ++} ++} ++#endif +diff --git a/auth/src/include/firebase/auth.h b/auth/src/include/firebase/auth.h +index bec3ce8f..71bdaa12 100644 +--- a/auth/src/include/firebase/auth.h ++++ b/auth/src/include/firebase/auth.h +@@ -147,6 +147,13 @@ class Auth { + + ~Auth(); + ++#if defined(__swift__) ++#if FIREBASE_PLATFORM_WINDOWS ++ // TODO(apple/swift#67288) support trivial C++ types with non-trivial dtors ++ Auth(const Auth &) noexcept; ++#endif ++#endif ++ + /// Synchronously gets the cached current user, or returns an object where + /// is_valid() == false if there is none. + /// +-- +2.39.2.windows.1 +