diff --git a/.github/workflows/swiftui-auth.yml b/.github/workflows/swiftui-auth.yml index 4b65be8fad..7faaacd95c 100644 --- a/.github/workflows/swiftui-auth.yml +++ b/.github/workflows/swiftui-auth.yml @@ -26,7 +26,7 @@ jobs: unit-tests: name: Package Unit Tests runs-on: macos-15 - timeout-minutes: 20 + timeout-minutes: 15 steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 @@ -129,7 +129,7 @@ jobs: ui-tests: name: UI Tests runs-on: macos-15 - timeout-minutes: 30 + timeout-minutes: 40 steps: - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 @@ -179,16 +179,16 @@ jobs: -enableCodeCoverage YES \ -resultBundlePath FirebaseSwiftUIExampleUITests.xcresult | tee FirebaseSwiftUIExampleUITests.log | xcpretty --test --color --simple - - name: Upload test logs + - name: Upload Firebase Emulator logs if: failure() uses: actions/upload-artifact@v4 with: - name: ui-tests-logs - path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.log + name: firebase-emulator-logs + path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/firebase-debug.log - name: Upload test results if: failure() uses: actions/upload-artifact@v4 with: - name: ui-tests-results + name: FirebaseSwiftUIExampleUITests.xcresult path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult \ No newline at end of file diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/Contents.json b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/Contents.json new file mode 100644 index 0000000000..6cc12269b3 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/Contents.json @@ -0,0 +1,7 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} + diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/Contents.json b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/Contents.json new file mode 100644 index 0000000000..ac6bbbb149 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/Contents.json @@ -0,0 +1,28 @@ +{ + "images" : [ + { + "filename" : "ic_github.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_github@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_github@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} + diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github.png new file mode 100644 index 0000000000..cae7a5ddfc Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github@2x.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github@2x.png new file mode 100644 index 0000000000..acd642bc19 Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github@2x.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github@3x.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github@3x.png new file mode 100644 index 0000000000..ffcfd198a4 Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/github_logo.imageset/ic_github@3x.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/Contents.json b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/Contents.json new file mode 100644 index 0000000000..60fe21a8d5 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/Contents.json @@ -0,0 +1,28 @@ +{ + "images" : [ + { + "filename" : "ic_microsoft.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_microsoft@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_microsoft@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} + diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft.png new file mode 100644 index 0000000000..3a57a3e967 Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft@2x.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft@2x.png new file mode 100644 index 0000000000..baf8979105 Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft@2x.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft@3x.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft@3x.png new file mode 100644 index 0000000000..ca78fa69de Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/microsoft_logo.imageset/ic_microsoft@3x.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/Contents.json b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/Contents.json new file mode 100644 index 0000000000..7504763768 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/Contents.json @@ -0,0 +1,28 @@ +{ + "images" : [ + { + "filename" : "ic_yahoo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_yahoo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_yahoo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} + diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo.png new file mode 100644 index 0000000000..3d0060050d Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo@2x.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo@2x.png new file mode 100644 index 0000000000..0deffb137a Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo@2x.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo@3x.png b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo@3x.png new file mode 100644 index 0000000000..f3bf49ed07 Binary files /dev/null and b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Resources/Media.xcassets/yahoo_logo.imageset/ic_yahoo@3x.png differ diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/AccountService+OAuth.swift b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/AccountService+OAuth.swift new file mode 100644 index 0000000000..54215c913d --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/AccountService+OAuth.swift @@ -0,0 +1,46 @@ +// +// AccountService+OAuth.swift +// FirebaseUI +// +// Created by Russell Wheatley on 21/10/2025. +// + +@preconcurrency import FirebaseAuth +import FirebaseAuthSwiftUI +import Observation + +protocol OAuthOperationReauthentication { + var oauthProvider: OAuthProviderSwift { get } +} + +extension OAuthOperationReauthentication { + @MainActor func reauthenticate() async throws -> AuthenticationToken { + guard let user = Auth.auth().currentUser else { + throw AuthServiceError.reauthenticationRequired("No user currently signed-in") + } + + do { + let credential = try await oauthProvider.createAuthCredential() + try await user.reauthenticate(with: credential) + + return .firebase("") + } catch { + throw AuthServiceError.signInFailed(underlying: error) + } + } +} + +@MainActor +class OAuthDeleteUserOperation: AuthenticatedOperation, + @preconcurrency OAuthOperationReauthentication { + let oauthProvider: OAuthProviderSwift + init(oauthProvider: OAuthProviderSwift) { + self.oauthProvider = oauthProvider + } + + func callAsFunction(on user: User) async throws { + try await callAsFunction(on: user) { + try await user.delete() + } + } +} diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/AuthService+OAuth.swift b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/AuthService+OAuth.swift new file mode 100644 index 0000000000..b4b6034af6 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/AuthService+OAuth.swift @@ -0,0 +1,30 @@ +// Copyright 2025 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. + +// +// AuthService+OAuth.swift +// FirebaseUI +// +// Created by Russell Wheatley on 21/10/2025. +// + +import FirebaseAuthSwiftUI + +public extension AuthService { + @discardableResult + func withOAuthSignIn(_ provider: OAuthProviderSwift) -> AuthService { + registerProvider(providerWithButton: OAuthProviderAuthUI(provider: provider)) + return self + } +} diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/OAuthProviderSwift+Presets.swift b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/OAuthProviderSwift+Presets.swift new file mode 100644 index 0000000000..080a575b50 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/OAuthProviderSwift+Presets.swift @@ -0,0 +1,65 @@ +// Copyright 2025 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. + +import SwiftUI + +/// Preset configurations for common OAuth providers +public extension OAuthProviderSwift { + /// GitHub OAuth provider + /// - Parameters: + /// - scopes: GitHub scopes (default: ["user"]) + /// - Returns: Configured GitHub provider + static func github(scopes: [String] = ["user"]) -> OAuthProviderSwift { + return OAuthProviderSwift( + providerId: "github.com", + scopes: scopes, + displayName: "Sign in with GitHub", + buttonIcon: Image("github_logo", bundle: .module), + buttonBackgroundColor: .black, + buttonForegroundColor: .white + ) + } + + /// Microsoft OAuth provider + /// - Parameters: + /// - scopes: Microsoft scopes (default: ["user.readwrite"]) + /// - Returns: Configured Microsoft provider + static func microsoft(scopes: [String] = ["user.readwrite"]) -> OAuthProviderSwift { + return OAuthProviderSwift( + providerId: "microsoft.com", + scopes: scopes, + customParameters: ["prompt": "consent"], + displayName: "Sign in with Microsoft", + buttonIcon: Image("microsoft_logo", bundle: .module), + buttonBackgroundColor: Color(red: 0 / 255, green: 120 / 255, blue: 212 / 255), + buttonForegroundColor: .white + ) + } + + /// Yahoo OAuth provider + /// - Parameters: + /// - scopes: Yahoo scopes (default: ["user.readwrite"]) + /// - Returns: Configured Yahoo provider + static func yahoo(scopes: [String] = ["user.readwrite"]) -> OAuthProviderSwift { + return OAuthProviderSwift( + providerId: "yahoo.com", + scopes: scopes, + customParameters: ["prompt": "consent"], + displayName: "Sign in with Yahoo", + buttonIcon: Image("yahoo_logo", bundle: .module), + buttonBackgroundColor: Color(red: 80 / 255, green: 0 / 255, blue: 155 / 255), + buttonForegroundColor: .white + ) + } +} diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/OAuthProviderSwift.swift b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/OAuthProviderSwift.swift new file mode 100644 index 0000000000..9e952b79f1 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/OAuthProviderSwift.swift @@ -0,0 +1,143 @@ +// Copyright 2025 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. + +import FirebaseAuth +import FirebaseAuthSwiftUI +import FirebaseCore +import SwiftUI + +/// Configuration for a generic OAuth provider +public class OAuthProviderSwift: AuthProviderSwift, DeleteUserSwift { + public let providerId: String + public let scopes: [String] + public let customParameters: [String: String] + + // Button appearance + public let displayName: String + public let buttonIcon: Image + public let buttonBackgroundColor: Color + public let buttonForegroundColor: Color + + /// Initialize a generic OAuth provider + /// - Parameters: + /// - providerId: The OAuth provider ID (e.g., "github.com", "microsoft.com") + /// - scopes: OAuth scopes to request + /// - customParameters: Additional OAuth parameters + /// - displayName: Button label (e.g., "Sign in with GitHub") + /// - buttonIcon: Button icon image + /// - buttonBackgroundColor: Button background color + /// - buttonForegroundColor: Button text/icon color + public init(providerId: String, + scopes: [String] = [], + customParameters: [String: String] = [:], + displayName: String, + buttonIcon: Image, + buttonBackgroundColor: Color = .black, + buttonForegroundColor: Color = .white) { + self.providerId = providerId + self.scopes = scopes + self.customParameters = customParameters + self.displayName = displayName + self.buttonIcon = buttonIcon + self.buttonBackgroundColor = buttonBackgroundColor + self.buttonForegroundColor = buttonForegroundColor + } + + /// Convenience initializer using SF Symbol + /// - Parameters: + /// - providerId: The OAuth provider ID (e.g., "github.com", "microsoft.com") + /// - scopes: OAuth scopes to request + /// - customParameters: Additional OAuth parameters + /// - displayName: Button label (e.g., "Sign in with GitHub") + /// - iconSystemName: SF Symbol name + /// - buttonBackgroundColor: Button background color + /// - buttonForegroundColor: Button text/icon color + public convenience init(providerId: String, + scopes: [String] = [], + customParameters: [String: String] = [:], + displayName: String, + iconSystemName: String, + buttonBackgroundColor: Color = .black, + buttonForegroundColor: Color = .white) { + self.init( + providerId: providerId, + scopes: scopes, + customParameters: customParameters, + displayName: displayName, + buttonIcon: Image(systemName: iconSystemName), + buttonBackgroundColor: buttonBackgroundColor, + buttonForegroundColor: buttonForegroundColor + ) + } + + @MainActor public func createAuthCredential() async throws -> AuthCredential { + let provider = OAuthProvider(providerID: providerId) + + // Set scopes if provided + if !scopes.isEmpty { + provider.scopes = scopes + } + + // Set custom parameters if provided + if !customParameters.isEmpty { + provider.customParameters = customParameters + } + + return try await withCheckedThrowingContinuation { continuation in + provider.getCredentialWith(nil) { credential, error in + if let error = error { + continuation.resume( + throwing: AuthServiceError.signInFailed(underlying: error) + ) + return + } + + guard let credential = credential else { + continuation.resume( + throwing: AuthServiceError.invalidCredentials( + "\(self.providerId) did not provide a valid AuthCredential" + ) + ) + return + } + + continuation.resume(returning: credential) + } + } + } + + public func deleteUser(user: User) async throws { + let operation = OAuthDeleteUserOperation(oauthProvider: self) + try await operation(on: user) + } +} + +public class OAuthProviderAuthUI: AuthProviderUI { + public var provider: AuthProviderSwift + + public init(provider: AuthProviderSwift) { + self.provider = provider + } + + public var id: String { + guard let oauthProvider = provider as? OAuthProviderSwift else { + return "oauth.unknown" + } + return oauthProvider.providerId + } + + @MainActor public func authButton() -> AnyView { + AnyView(GenericOAuthButton(provider: provider)) + } +} diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Views/GenericOAuthButton.swift b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Views/GenericOAuthButton.swift new file mode 100644 index 0000000000..0884dc8c8d --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Views/GenericOAuthButton.swift @@ -0,0 +1,64 @@ +// Copyright 2025 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. + +import FirebaseAuthSwiftUI +import SwiftUI + +/// A generic OAuth sign-in button that adapts to any provider's configuration +@MainActor +public struct GenericOAuthButton { + @Environment(AuthService.self) private var authService + let provider: AuthProviderSwift + + public init(provider: AuthProviderSwift) { + self.provider = provider + } +} + +extension GenericOAuthButton: View { + public var body: some View { + guard let oauthProvider = provider as? OAuthProviderSwift else { + return AnyView( + Text("Invalid OAuth Provider") + .foregroundColor(.red) + ) + } + + return AnyView( + Button(action: { + Task { + try await authService.signIn(provider) + } + }) { + HStack { + oauthProvider.buttonIcon + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 24, height: 24) + .foregroundColor(oauthProvider.buttonForegroundColor) + + Text(oauthProvider.displayName) + .fontWeight(.semibold) + .foregroundColor(oauthProvider.buttonForegroundColor) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(oauthProvider.buttonBackgroundColor) + .cornerRadius(8) + } + .accessibilityIdentifier("sign-in-with-\(oauthProvider.providerId)-button") + ) + } +} diff --git a/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Tests/FirebaseOAuthSwiftUITests/FirebaseOAuthSwiftUITests.swift b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Tests/FirebaseOAuthSwiftUITests/FirebaseOAuthSwiftUITests.swift new file mode 100644 index 0000000000..31b3bfd5bf --- /dev/null +++ b/FirebaseSwiftUI/FirebaseOAuthSwiftUI/Tests/FirebaseOAuthSwiftUITests/FirebaseOAuthSwiftUITests.swift @@ -0,0 +1,20 @@ +// Copyright 2025 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. + +@testable import FirebaseOAuthSwiftUI +import Testing + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/Package.swift b/Package.swift index c26c766008..3ab09585ea 100644 --- a/Package.swift +++ b/Package.swift @@ -86,6 +86,10 @@ let package = Package( name: "FirebaseAppleSwiftUI", targets: ["FirebaseAppleSwiftUI"] ), + .library( + name: "FirebaseOAuthSwiftUI", + targets: ["FirebaseOAuthSwiftUI"] + ), ], dependencies: [ .package( @@ -342,5 +346,20 @@ let package = Package( dependencies: ["FirebaseAppleSwiftUI"], path: "FirebaseSwiftUI/FirebaseAppleSwiftUI/Tests/" ), + .target( + name: "FirebaseOAuthSwiftUI", + dependencies: [ + "FirebaseAuthSwiftUI", + ], + path: "FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources", + resources: [ + .process("Resources") + ] + ), + .testTarget( + name: "FirebaseOAuthSwiftUITests", + dependencies: ["FirebaseOAuthSwiftUI"], + path: "FirebaseSwiftUI/FirebaseOAuthSwiftUI/Tests/" + ), ] ) diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj index d593bafac1..e0c6a0533d 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */; }; 4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */; }; 4610DD2A2EA796360084B32B /* FirebaseAppleSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4610DD292EA796360084B32B /* FirebaseAppleSwiftUI */; }; + 464938E92EA8E6BD0013A9E3 /* FirebaseOAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 464938E82EA8E6BD0013A9E3 /* FirebaseOAuthSwiftUI */; }; 4681E0002E97F22B00387C88 /* FirebaseTwitterSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */; }; 46CB7B252D773F2100F1FD0A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 46CB7B242D773F2100F1FD0A /* GoogleService-Info.plist */; }; 46F89C392D64B04E000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 46F89C382D64B04E000F8BC0 /* FirebaseAuthSwiftUI */; }; @@ -88,6 +89,7 @@ 8D808CB92DB081F900D2293F /* FirebasePhoneAuthSwiftUI in Frameworks */, 4681E0002E97F22B00387C88 /* FirebaseTwitterSwiftUI in Frameworks */, 4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */, + 464938E92EA8E6BD0013A9E3 /* FirebaseOAuthSwiftUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -167,6 +169,7 @@ 8D808CB82DB081F900D2293F /* FirebasePhoneAuthSwiftUI */, 4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */, 4610DD292EA796360084B32B /* FirebaseAppleSwiftUI */, + 464938E82EA8E6BD0013A9E3 /* FirebaseOAuthSwiftUI */, ); productName = FirebaseSwiftUIExample; productReference = 46F89C082D64A86C000F8BC0 /* FirebaseSwiftUIExample.app */; @@ -671,6 +674,11 @@ package = 8D808CB52DB07EBD00D2293F /* XCLocalSwiftPackageReference "../../../../FirebaseUI-iOS" */; productName = FirebaseAppleSwiftUI; }; + 464938E82EA8E6BD0013A9E3 /* FirebaseOAuthSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = 8D808CB52DB07EBD00D2293F /* XCLocalSwiftPackageReference "../../../../FirebaseUI-iOS" */; + productName = FirebaseOAuthSwiftUI; + }; 4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */ = { isa = XCSwiftPackageProductDependency; package = 8D808CB52DB07EBD00D2293F /* XCLocalSwiftPackageReference "../../../../FirebaseUI-iOS" */; diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift index 1101a55e36..3b1022a430 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift @@ -26,6 +26,7 @@ import FirebaseGoogleSwiftUI import FirebasePhoneAuthSwiftUI import FirebaseTwitterSwiftUI import FirebaseAppleSwiftUI +import FirebaseOAuthSwiftUI import SwiftUI struct ContentView: View { @@ -52,6 +53,9 @@ struct ContentView: View { .withPhoneSignIn() .withAppleSignIn() .withTwitterSignIn() + .withOAuthSignIn(OAuthProviderSwift.github()) + .withOAuthSignIn(OAuthProviderSwift.microsoft()) + .withOAuthSignIn(OAuthProviderSwift.yahoo()) .withFacebookSignIn() .withEmailSignIn() diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift index 04aef85482..ea1399cd02 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift @@ -26,6 +26,7 @@ import FirebaseGoogleSwiftUI import FirebasePhoneAuthSwiftUI import FirebaseAppleSwiftUI import FirebaseTwitterSwiftUI +import FirebaseOAuthSwiftUI import SwiftUI struct TestView: View { @@ -60,6 +61,9 @@ struct TestView: View { .withPhoneSignIn() .withAppleSignIn() .withTwitterSignIn() + .withOAuthSignIn(OAuthProviderSwift.github()) + .withOAuthSignIn(OAuthProviderSwift.microsoft()) + .withOAuthSignIn(OAuthProviderSwift.yahoo()) .withFacebookSignIn() .withEmailSignIn() } diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/start-firebase-emulator.sh b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/start-firebase-emulator.sh index fb1280e438..d5ee3dec67 100755 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/start-firebase-emulator.sh +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/start-firebase-emulator.sh @@ -14,7 +14,7 @@ if ! [ -x "$(command -v npm)" ]; then exit 1 fi -EMU_START_COMMAND="firebase emulators:start --only auth --project flutterfire-e2e-tests" +EMU_START_COMMAND="firebase emulators:start --only auth --project flutterfire-e2e-tests --debug" MAX_RETRIES=3 MAX_CHECKATTEMPTS=60 diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift index 7dbc0dc2f4..7d010d8777 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift @@ -61,6 +61,27 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase { appleButton.waitForExistence(timeout: 5), "Apple sign-in button should exist" ) + + // Check for Github sign-in button + let githubButton = app.buttons["sign-in-with-github.com-button"] + XCTAssertTrue( + githubButton.waitForExistence(timeout: 5), + "Github sign-in button should exist" + ) + + // Check for Microsoft sign-in button + let microsoftButton = app.buttons["sign-in-with-microsoft.com-button"] + XCTAssertTrue( + microsoftButton.waitForExistence(timeout: 5), + "Microsoft sign-in button should exist" + ) + + // Check for Yahoo sign-in button + let yahooButton = app.buttons["sign-in-with-yahoo.com-button"] + XCTAssertTrue( + yahooButton.waitForExistence(timeout: 5), + "Yahoo sign-in button should exist" + ) // Check for Google sign-in button let googleButton = app.buttons["sign-in-with-google-button"] @@ -215,6 +236,13 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase { XCTAssertTrue(signUpButton.exists, "Sign-Up button should exist") signUpButton.tap() + // Wait for the auth screen to disappear (email field should no longer exist) + let emailFieldDisappeared = NSPredicate(format: "exists == false") + let expectation = XCTNSPredicateExpectation(predicate: emailFieldDisappeared, object: emailField) + let result = XCTWaiter().wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(result, .completed, "Email field should disappear after sign-up") + + // Wait for user creation and signed-in view to appear let signedInText = app.staticTexts["signed-in-text"] XCTAssertTrue(