diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index c5ecdc1643..4a13cff90f 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -14,6 +14,10 @@ public protocol PhoneAuthProviderProtocol { @MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String } +public protocol TwitterProviderProtocol { + @MainActor func signInWithTwitter() async throws -> AuthCredential +} + public enum AuthenticationState { case unauthenticated case authenticating @@ -63,12 +67,14 @@ public final class AuthService { public init(configuration: AuthConfiguration = AuthConfiguration(), auth: Auth = Auth.auth(), googleProvider: GoogleProviderProtocol? = nil, facebookProvider: FacebookProviderProtocol? = nil, - phoneAuthProvider: PhoneAuthProviderProtocol? = nil) { + phoneAuthProvider: PhoneAuthProviderProtocol? = nil, + twitterProvider: TwitterProviderProtocol? = nil) { self.auth = auth self.configuration = configuration self.googleProvider = googleProvider self.facebookProvider = facebookProvider self.phoneAuthProvider = phoneAuthProvider + self.twitterProvider = twitterProvider string = StringUtils(bundle: configuration.customStringsBundle ?? Bundle.module) listenerManager = AuthListenerManager(auth: auth, authEnvironment: self) } @@ -88,6 +94,7 @@ public final class AuthService { private let googleProvider: GoogleProviderProtocol? private let facebookProvider: FacebookProviderProtocol? private let phoneAuthProvider: PhoneAuthProviderProtocol? + private let twitterProvider: TwitterProviderProtocol? private var safeGoogleProvider: GoogleProviderProtocol { get throws { @@ -119,6 +126,16 @@ public final class AuthService { } } + private var safeTwitterProvider: TwitterProviderProtocol { + get throws { + guard let provider = twitterProvider else { + throw AuthServiceError + .notConfiguredProvider("`TwitterProviderSwift` has not been configured") + } + return provider + } + } + private func safeActionCodeSettings() throws -> ActionCodeSettings { // email sign-in requires action code settings guard let actionCodeSettings = configuration @@ -318,6 +335,16 @@ public extension AuthService { } } +// MARK: - Twitter Sign In + +public extension AuthService { + func signInWithTwitter() async throws { + let credential = try await safeTwitterProvider + .signInWithTwitter() + try await signIn(credentials: credential) + } +} + // MARK: - Phone Auth Sign In public extension AuthService { diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/.gitignore b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Assets/ic_twitter-white.png b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Assets/ic_twitter-white.png new file mode 100644 index 0000000000..2609e58006 Binary files /dev/null and b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Assets/ic_twitter-white.png differ diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Assets/ic_twitter_black.png b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Assets/ic_twitter_black.png new file mode 100644 index 0000000000..2c2d7e863f Binary files /dev/null and b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Assets/ic_twitter_black.png differ diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/TwitterProviderSwift.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/TwitterProviderSwift.swift new file mode 100644 index 0000000000..d93883f028 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/TwitterProviderSwift.swift @@ -0,0 +1,30 @@ +import FirebaseAuth +import FirebaseAuthSwiftUI + +public class TwitterProviderSwift: TwitterProviderProtocol { + let scopes: [String] + let shortName = "Twitter" + let providerId = "twitter.com" + + public init(scopes: [String]? = nil) { + self.scopes = scopes ?? ["user.readwrite"] + } + + @MainActor public func signInWithTwitter() async throws -> AuthCredential { + let provider = OAuthProvider(providerID: providerId) + return try await withCheckedThrowingContinuation { continuation in + provider.getCredentialWith(nil) { credential, error in + if let error { + continuation + .resume(throwing: AuthServiceError.signInFailed(underlying: error)) + } else if let credential { + continuation.resume(returning: credential) + } else { + continuation + .resume(throwing: AuthServiceError + .invalidCredentials("Twitter did not provide a valid AuthCredential")) + } + } + } + } +} diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Views/SignInWithTwitterButton.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Views/SignInWithTwitterButton.swift new file mode 100644 index 0000000000..92d09c8c59 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Views/SignInWithTwitterButton.swift @@ -0,0 +1,52 @@ + +import FirebaseAuthSwiftUI +import SwiftUI + +@MainActor +public struct SignInWithTwitterButton { + @Environment(AuthService.self) private var authService + + public init() {} + + private func signInWithTwitter() async { + do { + try await authService.signInWithTwitter() + } catch {} + } +} + +extension SignInWithTwitterButton: View { + public var body: some View { + Button(action: { + Task { + try await signInWithTwitter() + } + }) { + if authService.authenticationState != .authenticating { + HStack { + Image("ic_twitter_black", bundle: .module) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.leading, 8) + + Text(authService.authenticationFlow == .login + ? "Login with Twitter" + : "Sign up with Twitter") + .foregroundColor(.white) + .fontWeight(.semibold) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + } + .background(Color(red: 29 / 255, green: 161 / 255, blue: 242 / 255)) + .cornerRadius(8) + .accessibilityLabel("Sign in with Twitter") + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + } + } +} diff --git a/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/FirebaseTwitterSwiftUITests/FirebaseTwitterSwiftUITests.swift b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/FirebaseTwitterSwiftUITests/FirebaseTwitterSwiftUITests.swift new file mode 100644 index 0000000000..54244a41e7 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/FirebaseTwitterSwiftUITests/FirebaseTwitterSwiftUITests.swift @@ -0,0 +1,6 @@ +@testable import FirebaseTwitterSwiftUI +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 a629a986fb..97622d7651 100644 --- a/Package.swift +++ b/Package.swift @@ -78,6 +78,10 @@ let package = Package( name: "FirebasePhoneAuthSwiftUI", targets: ["FirebasePhoneAuthSwiftUI"] ), + .library( + name: "FirebaseTwitterSwiftUI", + targets: ["FirebaseTwitterSwiftUI"] + ), ], dependencies: [ .package( @@ -311,5 +315,20 @@ let package = Package( dependencies: ["FirebasePhoneAuthSwiftUI"], path: "FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Tests/" ), + .target( + name: "FirebaseTwitterSwiftUI", + dependencies: [ + "FirebaseAuthSwiftUI", + ], + path: "FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources", + resources: [ + .process("Assets"), + ] + ), + .testTarget( + name: "FirebaseTwitterSwiftUITests", + dependencies: ["FirebaseTwitterSwiftUI"], + path: "FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/" + ), ] ) diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj index 390e704b85..8aec356ba4 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */; }; 4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */; }; 4670DEA72D9EA9E100E0D36A /* FirebaseFacebookSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4670DEA62D9EA9E100E0D36A /* FirebaseFacebookSwiftUI */; }; + 4674FC322DB12C5400299796 /* FirebaseTwitterSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4674FC312DB12C5400299796 /* FirebaseTwitterSwiftUI */; }; 46C4EAB32DA801B200FC878B /* FirebasePhoneAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 46C4EAB22DA801B200FC878B /* FirebasePhoneAuthSwiftUI */; }; 46CB7B252D773F2100F1FD0A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 46CB7B242D773F2100F1FD0A /* GoogleService-Info.plist */; }; 46F89C392D64B04E000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 46F89C382D64B04E000F8BC0 /* FirebaseAuthSwiftUI */; }; @@ -81,6 +82,7 @@ 46F89C4D2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */, 4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */, 46C4EAB32DA801B200FC878B /* FirebasePhoneAuthSwiftUI in Frameworks */, + 4674FC322DB12C5400299796 /* FirebaseTwitterSwiftUI in Frameworks */, 4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -157,6 +159,7 @@ 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */, 4670DEA62D9EA9E100E0D36A /* FirebaseFacebookSwiftUI */, 46C4EAB22DA801B200FC878B /* FirebasePhoneAuthSwiftUI */, + 4674FC312DB12C5400299796 /* FirebaseTwitterSwiftUI */, ); productName = FirebaseSwiftUIExample; productReference = 46F89C082D64A86C000F8BC0 /* FirebaseSwiftUIExample.app */; @@ -637,6 +640,11 @@ package = 4607CC9A2D9BFE29009EC3F5 /* XCLocalSwiftPackageReference "../../../../firebaseUI-ios" */; productName = FirebaseFacebookSwiftUI; }; + 4674FC312DB12C5400299796 /* FirebaseTwitterSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = 4607CC9A2D9BFE29009EC3F5 /* XCLocalSwiftPackageReference "../../../../firebaseUI-ios" */; + productName = FirebaseTwitterSwiftUI; + }; 46C4EAB22DA801B200FC878B /* FirebasePhoneAuthSwiftUI */ = { isa = XCSwiftPackageProductDependency; package = 4607CC9A2D9BFE29009EC3F5 /* XCLocalSwiftPackageReference "../../../../firebaseUI-ios" */; diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift index c5f85d500f..a02421fcff 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift @@ -12,6 +12,7 @@ import FirebaseCore import FirebaseFacebookSwiftUI import FirebaseGoogleSwiftUI import FirebasePhoneAuthSwiftUI +import FirebaseTwitterSwiftUI import SwiftData import SwiftUI @@ -92,11 +93,13 @@ struct ContentView: View { ) let facebookProvider = FacebookProviderSwift() let phoneAuthProvider = PhoneAuthProviderSwift() + let twitterProvider = TwitterProviderSwift() authService = AuthService( configuration: configuration, googleProvider: googleProvider, facebookProvider: facebookProvider, - phoneAuthProvider: phoneAuthProvider + phoneAuthProvider: phoneAuthProvider, + twitterProvider: twitterProvider ) } @@ -104,6 +107,7 @@ struct ContentView: View { AuthPickerView { SignInWithGoogleButton() SignInWithFacebookButton() + SignInWithTwitterButton() PhoneAuthButtonView() }.environment(authService) }