diff --git a/.gitignore b/.gitignore index 613935b581..7b68a9a26f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ DerivedData *.hmap *.ipa *.xcuserstate - +.build/ # Third Party /sdk diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000000..2dfdf2f304 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,5 @@ +# Formatting Options +--indent 2 +--maxwidth 100 +--wrapparameters afterfirst +--disable wrapMultilineStatementBraces \ No newline at end of file diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/.gitignore b/FirebaseSwiftUI/FirebaseAuthSwiftUI/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/.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/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift new file mode 100644 index 0000000000..7302beeda7 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift @@ -0,0 +1,31 @@ + +import SwiftUI + +public enum AuthServiceError: LocalizedError { + case invalidEmailLink + case notConfiguredProvider(String) + case clientIdNotFound(String) + case notConfiguredActionCodeSettings + case reauthenticationRequired(String) + case invalidCredentials(String) + case signInFailed(underlying: Error) + + public var errorDescription: String? { + switch self { + case .invalidEmailLink: + return "Invalid sign in link. Most likely, the link you used has expired. Try signing in again." + case let .notConfiguredProvider(description): + return description + case let .clientIdNotFound(description): + return description + case .notConfiguredActionCodeSettings: + return "ActionCodeSettings has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`" + case let .reauthenticationRequired(description): + return description + case let .invalidCredentials(description): + return description + case let .signInFailed(underlying: error): + return "Failed to sign in: \(error.localizedDescription)" + } + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift new file mode 100644 index 0000000000..d5a91ab2c3 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift @@ -0,0 +1,68 @@ +@preconcurrency import FirebaseAuth +import Observation + +protocol EmailPasswordOperationReauthentication { + var passwordPrompt: PasswordPromptCoordinator { get } +} + +extension EmailPasswordOperationReauthentication { + func reauthenticate() async throws -> AuthenticationToken { + guard let user = Auth.auth().currentUser else { + throw AuthServiceError.reauthenticationRequired("No user currently signed-in") + } + + guard let email = user.email else { + throw AuthServiceError.invalidCredentials("User does not have an email address") + } + + do { + let password = try await passwordPrompt.confirmPassword() + + let credential = EmailAuthProvider.credential(withEmail: email, password: password) + try await Auth.auth().currentUser?.reauthenticate(with: credential) + + return .firebase("") + } catch { + throw AuthServiceError.signInFailed(underlying: error) + } + } +} + +class EmailPasswordDeleteUserOperation: DeleteUserOperation, + EmailPasswordOperationReauthentication { + let passwordPrompt: PasswordPromptCoordinator + + init(passwordPrompt: PasswordPromptCoordinator) { + self.passwordPrompt = passwordPrompt + } +} + +@MainActor +@Observable +public final class PasswordPromptCoordinator { + var isPromptingPassword = false + private var continuation: CheckedContinuation? + + func confirmPassword() async throws -> String { + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + self.isPromptingPassword = true + } + } + + func submit(password: String) { + continuation?.resume(returning: password) + cleanup() + } + + func cancel() { + continuation? + .resume(throwing: AuthServiceError.reauthenticationRequired("Password entry cancelled")) + cleanup() + } + + private func cleanup() { + continuation = nil + isPromptingPassword = false + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift new file mode 100644 index 0000000000..5e48484248 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift @@ -0,0 +1,45 @@ +import AuthenticationServices +import FirebaseAuth + +extension NSError { + var requiresReauthentication: Bool { + domain == AuthErrorDomain && code == AuthErrorCode.requiresRecentLogin.rawValue + } + + var credentialAlreadyInUse: Bool { + domain == AuthErrorDomain && code == AuthErrorCode.credentialAlreadyInUse.rawValue + } +} + +enum AuthenticationToken { + case apple(ASAuthorizationAppleIDCredential, String) + case firebase(String) +} + +protocol AuthenticatedOperation { + func callAsFunction(on user: User) async throws + func reauthenticate() async throws -> AuthenticationToken + func performOperation(on user: User, with token: AuthenticationToken?) async throws +} + +extension AuthenticatedOperation { + func callAsFunction(on user: User) async throws { + do { + try await performOperation(on: user, with: nil) + } catch let error as NSError where error.requiresReauthentication { + let token = try await reauthenticate() + try await performOperation(on: user, with: token) + } catch AuthServiceError.reauthenticationRequired { + let token = try await reauthenticate() + try await performOperation(on: user, with: token) + } + } +} + +protocol DeleteUserOperation: AuthenticatedOperation {} + +extension DeleteUserOperation { + func performOperation(on user: User, with _: AuthenticationToken? = nil) async throws { + try await user.delete() + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift new file mode 100644 index 0000000000..5c3287f566 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift @@ -0,0 +1,31 @@ +import FirebaseAuth +import Foundation + +public struct AuthConfiguration { + let shouldHideCancelButton: Bool + let interactiveDismissEnabled: Bool + let shouldAutoUpgradeAnonymousUsers: Bool + let customStringsBundle: Bundle? + let tosUrl: URL + let privacyPolicyUrl: URL + let emailLinkSignInActionCodeSettings: ActionCodeSettings? + let verifyEmailActionCodeSettings: ActionCodeSettings? + + public init(shouldHideCancelButton: Bool = false, + interactiveDismissEnabled: Bool = true, + shouldAutoUpgradeAnonymousUsers: Bool = false, + customStringsBundle: Bundle? = nil, + tosUrl: URL = URL(string: "https://example.com/tos")!, + privacyPolicyUrl: URL = URL(string: "https://example.com/privacy")!, + emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil, + verifyEmailActionCodeSettings: ActionCodeSettings? = nil) { + self.shouldHideCancelButton = shouldHideCancelButton + self.interactiveDismissEnabled = interactiveDismissEnabled + self.shouldAutoUpgradeAnonymousUsers = shouldAutoUpgradeAnonymousUsers + self.customStringsBundle = customStringsBundle + self.tosUrl = tosUrl + self.privacyPolicyUrl = privacyPolicyUrl + self.emailLinkSignInActionCodeSettings = emailLinkSignInActionCodeSettings + self.verifyEmailActionCodeSettings = verifyEmailActionCodeSettings + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift new file mode 100644 index 0000000000..c5ecdc1643 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -0,0 +1,340 @@ +@preconcurrency import FirebaseAuth +import SwiftUI + +public protocol GoogleProviderProtocol { + func handleUrl(_ url: URL) -> Bool + @MainActor func signInWithGoogle(clientID: String) async throws -> AuthCredential +} + +public protocol FacebookProviderProtocol { + @MainActor func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential +} + +public protocol PhoneAuthProviderProtocol { + @MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String +} + +public enum AuthenticationState { + case unauthenticated + case authenticating + case authenticated +} + +public enum AuthenticationFlow { + case login + case signUp +} + +public enum AuthView { + case authPicker + case passwordRecovery + case emailLink +} + +@MainActor +private final class AuthListenerManager { + private var authStateHandle: AuthStateDidChangeListenerHandle? + private let auth: Auth + private weak var authEnvironment: AuthService? + + init(auth: Auth, authEnvironment: AuthService) { + self.auth = auth + self.authEnvironment = authEnvironment + setupAuthenticationListener() + } + + deinit { + if let handle = authStateHandle { + auth.removeStateDidChangeListener(handle) + } + } + + private func setupAuthenticationListener() { + authStateHandle = auth.addStateDidChangeListener { [weak self] _, user in + self?.authEnvironment?.currentUser = user + self?.authEnvironment?.updateAuthenticationState() + } + } +} + +@MainActor +@Observable +public final class AuthService { + public init(configuration: AuthConfiguration = AuthConfiguration(), auth: Auth = Auth.auth(), + googleProvider: GoogleProviderProtocol? = nil, + facebookProvider: FacebookProviderProtocol? = nil, + phoneAuthProvider: PhoneAuthProviderProtocol? = nil) { + self.auth = auth + self.configuration = configuration + self.googleProvider = googleProvider + self.facebookProvider = facebookProvider + self.phoneAuthProvider = phoneAuthProvider + string = StringUtils(bundle: configuration.customStringsBundle ?? Bundle.module) + listenerManager = AuthListenerManager(auth: auth, authEnvironment: self) + } + + @ObservationIgnored @AppStorage("email-link") public var emailLink: String? + public let configuration: AuthConfiguration + public let auth: Auth + public var authView: AuthView = .authPicker + public let string: StringUtils + public var currentUser: User? + public var authenticationState: AuthenticationState = .unauthenticated + public var authenticationFlow: AuthenticationFlow = .login + public var errorMessage = "" + public let passwordPrompt: PasswordPromptCoordinator = .init() + + private var listenerManager: AuthListenerManager? + private let googleProvider: GoogleProviderProtocol? + private let facebookProvider: FacebookProviderProtocol? + private let phoneAuthProvider: PhoneAuthProviderProtocol? + + private var safeGoogleProvider: GoogleProviderProtocol { + get throws { + guard let provider = googleProvider else { + throw AuthServiceError + .notConfiguredProvider("`GoogleProviderSwift` has not been configured") + } + return provider + } + } + + private var safeFacebookProvider: FacebookProviderProtocol { + get throws { + guard let provider = facebookProvider else { + throw AuthServiceError + .notConfiguredProvider("`FacebookProviderSwift` has not been configured") + } + return provider + } + } + + private var safePhoneAuthProvider: PhoneAuthProviderProtocol { + get throws { + guard let provider = phoneAuthProvider else { + throw AuthServiceError + .notConfiguredProvider("`PhoneAuthProviderSwift` has not been configured") + } + return provider + } + } + + private func safeActionCodeSettings() throws -> ActionCodeSettings { + // email sign-in requires action code settings + guard let actionCodeSettings = configuration + .emailLinkSignInActionCodeSettings else { + throw AuthServiceError + .notConfiguredActionCodeSettings + } + return actionCodeSettings + } + + public func updateAuthenticationState() { + reset() + authenticationState = + (currentUser == nil || currentUser?.isAnonymous == true) + ? .unauthenticated + : .authenticated + } + + func reset() { + errorMessage = "" + } + + public func signOut() async throws { + do { + try await auth.signOut() + updateAuthenticationState() + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } + + public func linkAccounts(credentials credentials: AuthCredential) async throws { + authenticationState = .authenticating + do { + try await currentUser?.link(with: credentials) + updateAuthenticationState() + } catch { + authenticationState = .unauthenticated + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } + + public func signIn(credentials credentials: AuthCredential) async throws { + authenticationState = .authenticating + if currentUser?.isAnonymous == true, configuration.shouldAutoUpgradeAnonymousUsers { + try await linkAccounts(credentials: credentials) + } else { + do { + try await auth.signIn(with: credentials) + updateAuthenticationState() + } catch { + authenticationState = .unauthenticated + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } + } + + func sendEmailVerification() async throws { + if currentUser != nil { + do { + // TODO: - can use set user action code settings? + try await currentUser!.sendEmailVerification() + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } + } +} + +// MARK: - User API + +public extension AuthService { + func deleteUser() async throws { + do { + if let user = auth.currentUser { + let operation = EmailPasswordDeleteUserOperation(passwordPrompt: passwordPrompt) + try await operation(on: user) + } + + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } +} + +// MARK: - Email/Password Sign In + +public extension AuthService { + func signIn(withEmail email: String, password: String) async throws { + let credential = EmailAuthProvider.credential(withEmail: email, password: password) + try await signIn(credentials: credential) + } + + func createUser(withEmail email: String, password: String) async throws { + authenticationState = .authenticating + + do { + try await auth.createUser(withEmail: email, password: password) + updateAuthenticationState() + } catch { + authenticationState = .unauthenticated + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } + + func sendPasswordRecoveryEmail(to email: String) async throws { + do { + try await auth.sendPasswordReset(withEmail: email) + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } +} + +// MARK: - Email Link Sign In + +public extension AuthService { + func sendEmailSignInLink(to email: String) async throws { + do { + let actionCodeSettings = try safeActionCodeSettings() + try await auth.sendSignInLink( + toEmail: email, + actionCodeSettings: actionCodeSettings + ) + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } + + func handleSignInLink(url url: URL) async throws { + do { + guard let email = emailLink else { + throw AuthServiceError.invalidEmailLink + } + let link = url.absoluteString + if auth.isSignIn(withEmailLink: link) { + let result = try await auth.signIn(withEmail: email, link: link) + updateAuthenticationState() + emailLink = nil + } + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } +} + +// MARK: - Google Sign In + +public extension AuthService { + func signInWithGoogle() async throws { + guard let clientID = auth.app?.options.clientID else { + throw AuthServiceError + .clientIdNotFound( + "OAuth client ID not found. Please make sure Google Sign-In is enabled in the Firebase console. You may have to download a new GoogleService-Info.plist file after enabling Google Sign-In." + ) + } + let credential = try await safeGoogleProvider.signInWithGoogle(clientID: clientID) + + try await signIn(credentials: credential) + } +} + +// MARK: - Facebook Sign In + +public extension AuthService { + func signInWithFacebook(limitedLogin: Bool = true) async throws { + let credential = try await safeFacebookProvider + .signInWithFacebook(isLimitedLogin: limitedLogin) + try await signIn(credentials: credential) + } +} + +// MARK: - Phone Auth Sign In + +public extension AuthService { + func verifyPhoneNumber(phoneNumber: String) async throws -> String { + do { + return try await safePhoneAuthProvider.verifyPhoneNumber(phoneNumber: phoneNumber) + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } + + func signInWithPhoneNumber(verificationID: String, verificationCode: String) async throws { + let credential = PhoneAuthProvider.provider() + .credential(withVerificationID: verificationID, verificationCode: verificationCode) + try await signIn(credentials: credential) + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Strings/Localizable.xcstrings b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Strings/Localizable.xcstrings new file mode 100644 index 0000000000..a4b95dde64 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Strings/Localizable.xcstrings @@ -0,0 +1,1124 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "AccountDisabledError" : { + "comment" : "Error message displayed when the account is disabled. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "migrated", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "That email address is for an account that has been disabled." + } + } + } + }, + "ActionCantBeUndone" : { + "comment" : "Alert message shown before account deletion.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This action can't be undone" + } + } + } + }, + "AddPasswordAlertMessage" : { + "comment" : "Alert message shown when adding account password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "To add password to your account, you will need to sign in again." + } + } + } + }, + "AddPasswordTitle" : { + "comment" : "Controller title shown when adding password to account.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add password" + } + } + } + }, + "Already have an account?" : { + + }, + "AS_AddPassword" : { + "comment" : "Account Settings cell title Add Password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add password" + } + } + } + }, + "AS_ChangePassword" : { + "comment" : "Account Settings cell title Change Password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change password" + } + } + } + }, + "AS_DeleteAccount" : { + "comment" : "Account Settings cell title Delete Account.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Account" + } + } + } + }, + "AS_Email" : { + "comment" : "Account Settings cell title Email. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Email" + } + } + } + }, + "AS_Name" : { + "comment" : "Account Settings cell title Name.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + } + } + }, + "AS_SectionLinkedAccounts" : { + "comment" : "Account Settings section title Linked Accounts.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linked Accounts" + } + } + } + }, + "AS_SectionProfile" : { + "comment" : "Account Settings section title Profile.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profile" + } + } + } + }, + "AS_SectionSecurity" : { + "comment" : "Account Settings section title Security.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Security" + } + } + } + }, + "AS_SignOut" : { + "comment" : "Account Settings cell title Sign Out.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Out" + } + } + } + }, + "AuthPickerTitle" : { + "comment" : "Title for auth picker screen.", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Welcome" + } + } + } + }, + "Back" : { + "comment" : "Back button title." + }, + "Cancel" : { + "comment" : "Cancel button title.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + } + } + }, + "CannotAuthenticateError" : { + "comment" : "Error message displayed when the app cannot authenticate user's account.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This type of account isn't supported by this app" + } + } + } + }, + "CantFindProvider" : { + "comment" : "Error message displayed when FUIAuth is not configured with third party provider. Parameter is value of provider (e g Google, Facebook etc)", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Can't find provider for %@." + } + } + } + }, + "ChoosePassword" : { + "comment" : "Placeholder for the password text field in a sign up form.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose password" + } + } + } + }, + "Close" : { + "comment" : "Alert button title Close.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Close" + } + } + } + }, + "Confirm password" : { + + }, + "ConfirmEmail" : { + "comment" : "Title of confirm email label.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm Email" + } + } + } + }, + "Delete" : { + "comment" : "Text of Delete action button.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete" + } + } + } + }, + "Delete account" : { + + }, + "DeleteAccountBody" : { + "comment" : "Alert message body shown to confirm account deletion action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This will erase all data associated with your account, and can't be undone You will need to sign in again to complete this action" + } + } + } + }, + "DeleteAccountConfirmationMessage" : { + "comment" : "Explanation message shown before deleting account.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This will erase all data associated with your account, and can't be undone. Are you sure you want to delete your account?" + } + } + } + }, + "DeleteAccountConfirmationTitle" : { + "comment" : "Alert message title shown to confirm account deletion action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Account?" + } + } + } + }, + "DeleteAccountControllerTitle" : { + "comment" : "Title of Controller shown before deleting account", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete account" + } + } + } + }, + "Dismiss" : { + + }, + "Don't have an account yet?" : { + + }, + "EditEmailTitle" : { + "comment" : "Controller title shown when editing account email. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit email" + } + } + } + }, + "EditNameTitle" : { + "comment" : "Controller title shown when editing account name.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit name" + } + } + } + }, + "EditPasswordAlertMessage" : { + "comment" : "Alert message shown when editing account password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "To change password to your account, you will need to sign in again." + } + } + } + }, + "EditPasswordTitle" : { + "comment" : "Controller title shown when editing password to account.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change password" + } + } + } + }, + "Email" : { + "comment" : "Label next to a email text field. Use short/abbreviated translation for 'email' which is less than 15 chars." + }, + "EmailAlreadyInUseError" : { + "comment" : "Error message displayed when the email address is already in use. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "migrated", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The email address is already in use by another account." + } + } + } + }, + "EmailsDontMatch" : { + "comment" : "Error message displayed when after re-authorization current user's email and re-authorized user's email doesn't match. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Emails don't match" + } + } + } + }, + "EmailSentConfirmationMessage" : { + "comment" : "Message displayed after email is sent. The placeholder is the email address that the email is sent to.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A sign-in email with additional instructions was sent to %@. Check your email to complete sign-in." + } + } + } + }, + "EnterYourEmail" : { + "comment" : "Title for email entry screen, email text field placeholder. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter your email" + } + } + } + }, + "EnterYourPassword" : { + "comment" : "Password text field placeholder.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter your password" + } + } + } + }, + "Error" : { + "comment" : "Alert title Error.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error" + } + } + } + }, + "ExistingAccountTitle" : { + "comment" : "Title of an alert shown to an existing user coming back to the app.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You already have an account" + } + } + } + }, + "FirstAndLastName" : { + "comment" : "Name text field placeholder.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "First & last name" + } + } + } + }, + "ForgotPassword" : { + "comment" : "Button text for 'Forgot Password' action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forgot password?" + } + } + } + }, + "ForgotPasswordTitle" : { + "comment" : "Title of forgot password button.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trouble signing in?" + } + } + } + }, + "Forgotten Password?" : { + + }, + "Instructions" : { + + }, + "InvalidEmailError" : { + "comment" : "Error message displayed when user enters an invalid email address. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "migrated", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "That email address isn't correct." + } + } + } + }, + "InvalidPasswordError" : { + "comment" : "Error message displayed when user enters an empty password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password cannot be empty." + } + } + } + }, + "Log in" : { + + }, + "Log in with password" : { + + }, + "Login" : { + + }, + "Name" : { + "comment" : "Label next to a name text field.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + } + } + }, + "Next" : { + "comment" : "Next button title.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next" + } + } + } + }, + "OK" : { + "comment" : "OK button title.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + } + } + }, + "Password" : { + "comment" : "Label next to a password text field." + }, + "Password Recovery" : { + + }, + "PasswordRecoveryEmailSentMessage" : { + "comment" : "Message displayed when the email for password recovery has been sent.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Follow the instructions sent to %@ to recover your password." + } + } + } + }, + "PasswordRecoveryEmailSentTitle" : { + "comment" : "Title of a message displayed when the email for password recovery has been sent. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check your email" + } + } + } + }, + "PasswordRecoveryMessage" : { + "comment" : "Explanation on how the password of an account can be recovered. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Get instructions sent to this email that explain how to reset your password." + } + } + } + }, + "PasswordRecoveryTitle" : { + "comment" : "Title for password recovery screen.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recover password" + } + } + } + }, + "PasswordVerificationMessage" : { + "comment" : "Message to explain to the user that password is needed for an account with this email address.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You’ve already used %@ to sign in. Enter your password for that account." + } + } + } + }, + "PlaceholderChosePassword" : { + "comment" : "Placeholder of secret input cell when user changes password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose password" + } + } + } + }, + "PlaceholderEnterEmail" : { + "comment" : "Placeholder of input cell when user changes name. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter your email" + } + } + } + }, + "PlaceholderEnterName" : { + "comment" : "Placeholder of input cell when user changes name.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter your name" + } + } + } + }, + "PlaceholderEnterPassword" : { + "comment" : "Placeholder of secret input cell when user changes password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter your password" + } + } + } + }, + "PlaceholderNewPassword" : { + "comment" : "Placeholder of secret input cell when user confirms password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New password" + } + } + } + }, + "Please check your email for email sign-in link." : { + + }, + "Please check your email for password recovery instructions." : { + + }, + "Please check your email for verification link." : { + + }, + "Prefer Email link sign-in?" : { + + }, + "PrivacyPolicy" : { + "comment" : "Text linked to a web page with the Privacy Policy content.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Policy" + } + } + } + }, + "ProviderTitleFacebook" : { + "comment" : "Title of Facebook provider", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Facebook" + } + } + } + }, + "ProviderTitleGoogle" : { + "comment" : "Title of Google provider", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Google" + } + } + } + }, + "ProviderTitlePassword" : { + "comment" : "Title of Password/Email provider. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Email" + } + } + } + }, + "ProviderTitleTwitter" : { + "comment" : "Title of Twitter provider", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twitter" + } + } + } + }, + "ProviderUsedPreviouslyMessage" : { + "comment" : "Alert message to let user know what identity provider (second placeholder, ex. Google) was used previously for the email address (first placeholder).", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You’ve already used %@. Sign in with %@ to continue." + } + } + } + }, + "ReauthenticateEditPasswordAlertMessage" : { + "comment" : "Alert message shown when re-authenticating before editing account password.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "In order to change your password, you first need to enter your current password." + } + } + } + }, + "Resend" : { + "comment" : "Resend button title.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resend" + } + } + } + }, + "Save" : { + "comment" : "Save button title.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save" + } + } + } + }, + "Send" : { + "comment" : "Send button title.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send" + } + } + } + }, + "Send email sign-in link" : { + + }, + "Sign in with email link" : { + + }, + "Sign out" : { + + }, + "Sign up" : { + + }, + "Signed in" : { + + }, + "SignedIn" : { + "comment" : "Title of successfully signed in label.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signed in!" + } + } + } + }, + "SignInEmailSent" : { + "comment" : "Message displayed after the email of sign-in link is sent.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign-in email Sent" + } + } + } + }, + "SignInTitle" : { + "comment" : "Title for sign in screen and sign in button.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign in" + } + } + } + }, + "SignInTooManyTimesError" : { + "comment" : "Error message displayed after user trying to sign in too many times.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You’ve entered an incorrect password too many times. Try again in a few minutes." + } + } + } + }, + "SignInWithEmail" : { + "comment" : "Sign in with email button label. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign in with email" + } + } + } + }, + "SignInWithProvider" : { + "comment" : "Sign in with provider button label.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign in with %@" + } + } + } + }, + "SignUpTitle" : { + "comment" : "Title for sign up screen.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Create account" + } + } + } + }, + "SignUpTooManyTimesError" : { + "comment" : "Error message displayed when many accounts have been created from same IP address.", + "extractionState" : "migrated", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Too many account requests are coming from your IP address. Try again in a few minutes." + } + } + } + }, + "TermsOfService" : { + "comment" : "Text linked to a web page with the Terms of Service content.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terms of Service" + } + } + } + }, + "TermsOfServiceMessage" : { + "comment" : "A message displayed when the first log in screen is displayed. The first placeholder is the terms of service agreement link, the second place holder is the privacy policy agreement link.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "By continuing, you are indicating that you accept our %@ and %@." + } + } + } + }, + "TroubleGettingEmailMessage" : { + "comment" : "Alert message displayed when user having trouble getting email.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Try these common fixes: \n - Check if the email was marked as spam or filtered.\n - Check your internet connection.\n - Check that you did not misspell your email.\n - Check that your inbox space is not running out or other inbox settings related issues.\n If the steps above didn't work, you can resend the email. Note that this will deactivate the link in the older email." + } + } + } + }, + "TroubleGettingEmailTitle" : { + "comment" : "Title used in trouble getting email alert view.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trouble getting emails?" + } + } + } + }, + "UnlinkAction" : { + "comment" : "Button title for unlinking account action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlink" + } + } + } + }, + "UnlinkConfirmationActionTitle" : { + "comment" : "Alert action title shown before unlinking action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlink account" + } + } + } + }, + "UnlinkConfirmationMessage" : { + "comment" : "Alert message shown before unlinking action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You will no longer be able to sign in using your account" + } + } + } + }, + "UnlinkConfirmationTitle" : { + "comment" : "Alert title shown before unlinking action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlink account?" + } + } + } + }, + "UnlinkTitle" : { + "comment" : "Controller title shown for unlinking account action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linked account" + } + } + } + }, + "UpdateEmailAlertMessage" : { + "comment" : "Alert action message shown before updating email action. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "To change email address associated with your account, you will need to sign in again." + } + } + } + }, + "UpdateEmailVerificationAlertMessage" : { + "comment" : "Alert action message shown before confirmation of updating email action.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "In order to change your password, you first need to enter your current password." + } + } + } + }, + "User: %@" : { + + }, + "UserNotFoundError" : { + "comment" : "Error message displayed when there's no account matching the email address. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "migrated", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "That email address doesn’t match an existing account." + } + } + } + }, + "Verify email address?" : { + + }, + "VerifyItsYou" : { + "comment" : "Alert message title show for re-authorization.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verify it's you" + } + } + } + }, + "WeakPasswordError" : { + "comment" : "Error message displayed when the password is too weak.", + "extractionState" : "migrated", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password must be at least 6 characters long." + } + } + } + }, + "WrongPasswordError" : { + "comment" : "Error message displayed when the email and password don't match. Use short/abbreviated translation for 'email' which is less than 15 chars.", + "extractionState" : "migrated", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The email and password you entered don't match." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/CommonUtils.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/CommonUtils.swift new file mode 100644 index 0000000000..308b12bd77 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/CommonUtils.swift @@ -0,0 +1,49 @@ +import CommonCrypto +import Foundation +import Security + +public class CommonUtils { + static let emailRegex = ".+@([a-zA-Z0-9\\-]+\\.)+[a-zA-Z0-9]{2,63}" + + public static func isValidEmail(_ email: String) -> Bool { + let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex) + return emailPredicate.evaluate(with: email) + } + + public static func randomNonce(length: Int = 32) -> String { + let characterSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._" + var result = "" + var remainingLength = length + + while remainingLength > 0 { + var randoms = [UInt8](repeating: 0, count: 16) + let errorCode = SecRandomCopyBytes(kSecRandomDefault, randoms.count, &randoms) + if errorCode != errSecSuccess { + fatalError("Unable to generate nonce: OSStatus \(errorCode)") + } + + for random in randoms { + if remainingLength == 0 { + break + } + + if random < characterSet.count { + let index = characterSet.index(characterSet.startIndex, offsetBy: Int(random)) + result.append(characterSet[index]) + remainingLength -= 1 + } + } + } + + return result + } + + public static func sha256Hash(of input: String) -> String { + guard let data = input.data(using: .utf8) else { return "" } + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) + } + return hash.map { String(format: "%02x", $0) }.joined() + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift new file mode 100644 index 0000000000..3bcb9ca738 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift @@ -0,0 +1,60 @@ +import FirebaseAuth +import SwiftUI + +let kKeyNotFound = "Key not found" + +let kUsersNotFoundError = "UserNotFoundError" +let kEmailAlreadyInUseError = "EmailAlreadyInUseError" +let kInvalidEmailError = "InvalidEmailError" +let kWeakPasswordError = "WeakPasswordError" +let kSignUpTooManyTimesError = "SignUpTooManyTimesError" +let kWrongPasswordError = "WrongPasswordError" +let kAccountDisabledError = "AccountDisabledError" +let kEmailsDoNotMatchError = "EmailsDoNotMatchError" +let kUnknownError = "UnknownError" + +public class StringUtils { + let bundle: Bundle + init(bundle: Bundle) { + self.bundle = bundle + } + + public func localizedString(forKey key: String) -> String { + let keyLocale = String.LocalizationValue(key) + let value = String(localized: keyLocale, bundle: bundle) + return value + } + + public func localizedErrorMessage(for error: Error) -> String { + let authError = error as NSError + let errorCode = AuthErrorCode(rawValue: authError.code) + switch errorCode { + case .emailAlreadyInUse: + return localizedString( + forKey: kEmailAlreadyInUseError + ) + case .invalidEmail: + return localizedString(forKey: kInvalidEmailError) + case .weakPassword: + return localizedString(forKey: kWeakPasswordError) + case .tooManyRequests: + return localizedString( + forKey: kSignUpTooManyTimesError + ) + case .wrongPassword: + return localizedString( + forKey: kWrongPasswordError + ) + case .userNotFound: + return localizedString( + forKey: kUsersNotFoundError + ) + case .userDisabled: + return localizedString( + forKey: kAccountDisabledError + ) + default: + return error.localizedDescription + } + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift new file mode 100644 index 0000000000..517149ce98 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -0,0 +1,51 @@ +import SwiftUI + +@MainActor +public struct AuthPickerView { + @Environment(AuthService.self) private var authService + let providerButtons: () -> Content + + public init(@ViewBuilder providerButtons: @escaping () -> Content) { + self.providerButtons = providerButtons + } + + private func switchFlow() { + authService.authenticationFlow = authService + .authenticationFlow == .login ? .signUp : .login + } +} + +extension AuthPickerView: View { + public var body: some View { + VStack { + if authService.authenticationState == .authenticated { + SignedInView() + } else if authService.authView == .passwordRecovery { + PasswordRecoveryView() + } else if authService.authView == .emailLink { + EmailLinkView() + } else { + Text(authService.authenticationFlow == .login ? "Login" : "Sign up") + VStack { Divider() } + EmailAuthView() + providerButtons() + VStack { Divider() } + HStack { + Text(authService + .authenticationFlow == .login ? "Don't have an account yet?" : + "Already have an account?") + Button(action: { + withAnimation { + switchFlow() + } + }) { + Text(authService.authenticationFlow == .signUp ? "Log in" : "Sign up") + .fontWeight(.semibold) + .foregroundColor(.blue) + } + } + Text(authService.errorMessage).foregroundColor(.red) + } + } + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift new file mode 100644 index 0000000000..f09a043226 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift @@ -0,0 +1,134 @@ +// +// EmailPasswordView.swift +// FirebaseUI +// +// Created by Russell Wheatley on 20/03/2025. +// +import FirebaseAuth +import SwiftUI + +private enum FocusableField: Hashable { + case email + case password + case confirmPassword +} + +@MainActor +public struct EmailAuthView { + @Environment(AuthService.self) private var authService + + @State private var email = "" + @State private var password = "" + @State private var confirmPassword = "" + + @FocusState private var focus: FocusableField? + + public init() {} + + private var isValid: Bool { + return if authService.authenticationFlow == .login { + !email.isEmpty && !password.isEmpty + } else { + !email.isEmpty && !password.isEmpty && password == confirmPassword + } + } + + private func signInWithEmailPassword() async { + do { + try await authService.signIn(withEmail: email, password: password) + } catch {} + } + + private func createUserWithEmailPassword() async { + do { + try await authService.createUser(withEmail: email, password: password) + } catch {} + } +} + +extension EmailAuthView: View { + public var body: some View { + VStack { + LabeledContent { + TextField("Email", text: $email) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focus, equals: .email) + .submitLabel(.next) + .onSubmit { + self.focus = .password + } + } label: { + Image(systemName: "at") + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 4) + + LabeledContent { + SecureField("Password", text: $password) + .focused($focus, equals: .password) + .submitLabel(.go) + .onSubmit { + Task { await signInWithEmailPassword() } + } + } label: { + Image(systemName: "lock") + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 8) + + if authService.authenticationFlow == .login { + Button(action: { + authService.authView = .passwordRecovery + }) { + Text("Forgotten Password?") + } + } + + if authService.authenticationFlow == .signUp { + LabeledContent { + SecureField("Confirm password", text: $confirmPassword) + .focused($focus, equals: .confirmPassword) + .submitLabel(.go) + .onSubmit { + Task { await createUserWithEmailPassword() } + } + } label: { + Image(systemName: "lock") + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 8) + } + + Button(action: { + Task { + if authService.authenticationFlow == .login { await signInWithEmailPassword() } + else { await createUserWithEmailPassword() } + } + }) { + if authService.authenticationState != .authenticating { + Text(authService.authenticationFlow == .login ? "Log in with password" : "Sign up") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + } + .disabled(!isValid) + .padding([.top, .bottom], 8) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + Button(action: { + authService.authView = .passwordRecovery + }) { + Text("Prefer Email link sign-in?") + } + } + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift new file mode 100644 index 0000000000..b8dbcce8f6 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift @@ -0,0 +1,76 @@ +import FirebaseAuth +import SwiftUI + +public struct EmailLinkView { + @Environment(AuthService.self) private var authService + @State private var email = "" + @State private var showModal = false + + public init() {} + + private func sendEmailLink() async { + do { + try await authService.sendEmailSignInLink(to: email) + showModal = true + } catch {} + } +} + +extension EmailLinkView: View { + public var body: some View { + VStack { + Text("Sign in with email link") + LabeledContent { + TextField("Email", text: $email) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .submitLabel(.next) + } label: { + Image(systemName: "at") + }.padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 4) + Button(action: { + Task { + await sendEmailLink() + authService.emailLink = email + } + }) { + Text("Send email sign-in link") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .disabled(!CommonUtils.isValidEmail(email)) + .padding([.top, .bottom], 8) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + Text(authService.errorMessage).foregroundColor(.red) + }.sheet(isPresented: $showModal) { + VStack { + Text("Instructions") + .font(.headline) + Text("Please check your email for email sign-in link.") + .padding() + Button("Dismiss") { + showModal = false + } + .padding() + } + .padding() + }.onOpenURL { url in + Task { + do { + try await authService.handleSignInLink(url: url) + } catch {} + } + } + .navigationBarItems(leading: Button(action: { + authService.authView = .authPicker + }) { + Image(systemName: "chevron.left") + .foregroundColor(.blue) + Text("Back") + .foregroundColor(.blue) + }) + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift new file mode 100644 index 0000000000..0af5733d90 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift @@ -0,0 +1,29 @@ +import SwiftUI + +struct PasswordPromptSheet { + @Bindable var coordinator: PasswordPromptCoordinator + @State private var password = "" +} + +extension PasswordPromptSheet: View { + var body: some View { + VStack(spacing: 20) { + SecureField("Enter Password", text: $password) + .textFieldStyle(.roundedBorder) + .padding() + + HStack { + Button("Cancel") { + coordinator.cancel() + } + Spacer() + Button("Submit") { + coordinator.submit(password: password) + } + .disabled(password.isEmpty) + } + .padding(.horizontal) + } + .padding() + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift new file mode 100644 index 0000000000..c53a6969e7 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift @@ -0,0 +1,69 @@ +import SwiftUI + +public struct PasswordRecoveryView { + @Environment(AuthService.self) private var authService + @State private var email = "" + @State private var showModal = false + + public init() {} + + private func sendPasswordRecoveryEmail() async { + do { + try await authService.sendPasswordRecoveryEmail(to: email) + showModal = true + } catch {} + } +} + +extension PasswordRecoveryView: View { + public var body: some View { + VStack { + Text("Password Recovery") + LabeledContent { + TextField("Email", text: $email) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .submitLabel(.next) + } label: { + Image(systemName: "at") + }.padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 4) + Button(action: { + Task { + await sendPasswordRecoveryEmail() + } + }) { + Text("Password Recovery") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .disabled(!CommonUtils.isValidEmail(email)) + .padding([.top, .bottom], 8) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + }.sheet(isPresented: $showModal) { + VStack { + Text("Instructions") + .font(.headline) + Text("Please check your email for password recovery instructions.") + .padding() + Button("Dismiss") { + showModal = false + } + .padding() + } + .padding() + }.onOpenURL { _ in + // move the user to email/password View + } + .navigationBarItems(leading: Button(action: { + authService.authView = .authPicker + }) { + Image(systemName: "chevron.left") + .foregroundColor(.blue) + Text("Back") + .foregroundColor(.blue) + }) + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift new file mode 100644 index 0000000000..6b27315eb1 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift @@ -0,0 +1,45 @@ +import SwiftUI + +@MainActor +public struct SignedInView { + @Environment(AuthService.self) private var authService +} + +extension SignedInView: View { + private var isShowingPasswordPrompt: Binding { + Binding( + get: { authService.passwordPrompt.isPromptingPassword }, + set: { authService.passwordPrompt.isPromptingPassword = $0 } + ) + } + + public var body: some View { + VStack { + Text("Signed in") + Text("User: \(authService.currentUser?.email ?? "Unknown")") + + if authService.currentUser?.isEmailVerified == false { + VerifyEmailView() + } + + Button("Sign out") { + Task { + do { + try await authService.signOut() + } catch {} + } + } + Divider() + Button("Delete account") { + Task { + do { + try await authService.deleteUser() + } catch {} + } + } + Text(authService.errorMessage).foregroundColor(.red) + }.sheet(isPresented: isShowingPasswordPrompt) { + PasswordPromptSheet(coordinator: authService.passwordPrompt) + } + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/VerifyEmailView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/VerifyEmailView.swift new file mode 100644 index 0000000000..99014a47ab --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/VerifyEmailView.swift @@ -0,0 +1,44 @@ +import SwiftUI + +public struct VerifyEmailView { + @Environment(AuthService.self) private var authService + @State private var showModal = false + + private func sendEmailVerification() async { + do { + try await authService.sendEmailVerification() + showModal = true + } catch {} + } +} + +extension VerifyEmailView: View { + public var body: some View { + VStack { + Button(action: { + Task { + await sendEmailVerification() + } + }) { + Text("Verify email address?") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .padding([.top, .bottom], 8) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + }.sheet(isPresented: $showModal) { + VStack { + Text("Instructions") + .font(.headline) + Text("Please check your email for verification link.") + .padding() + Button("Dismiss") { + showModal = false + } + .padding() + } + .padding() + } + } +} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/FirebaseAuthSwiftUITests/FirebaseAuthSwiftUITests.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/FirebaseAuthSwiftUITests/FirebaseAuthSwiftUITests.swift new file mode 100644 index 0000000000..81d1f47565 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/FirebaseAuthSwiftUITests/FirebaseAuthSwiftUITests.swift @@ -0,0 +1,6 @@ +@testable import FirebaseAuthSwiftUI +import Testing + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/.gitignore b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/.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/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderSwift.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderSwift.swift new file mode 100644 index 0000000000..c72887efae --- /dev/null +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderSwift.swift @@ -0,0 +1,115 @@ +import AppTrackingTransparency +import FacebookCore +import FacebookLogin +import FirebaseAuth +import FirebaseAuthSwiftUI + +let kFacebookEmailScope = "email" +let kFacebookProfileScope = "public_profile" +let kDefaultFacebookScopes = [kFacebookEmailScope, kFacebookProfileScope] + +public enum FacebookLoginType { + case classic + case limitedLogin +} + +public enum FacebookProviderError: Error { + case signInCancelled(String) + case configurationInvalid(String) + case accessToken(String) + case authenticationToken(String) +} + +public class FacebookProviderSwift: FacebookProviderProtocol { + let scopes: [String] + let shortName = "Facebook" + let providerId = "facebook.com" + private let loginManager = LoginManager() + private var rawNonce: String + private var shaNonce: String + + public init(scopes: [String]? = nil) { + self.scopes = scopes ?? kDefaultFacebookScopes + rawNonce = CommonUtils.randomNonce() + shaNonce = CommonUtils.sha256Hash(of: rawNonce) + } + + @MainActor public func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential { + let trackingStatus = ATTrackingManager.trackingAuthorizationStatus + let tracking: LoginTracking = trackingStatus != .authorized ? .limited : + (isLimitedLogin ? .limited : .enabled) + + guard let configuration: LoginConfiguration = { + if tracking == .limited { + return LoginConfiguration( + permissions: scopes, + tracking: tracking, + nonce: shaNonce + ) + } else { + return LoginConfiguration( + permissions: scopes, + tracking: tracking + ) + } + }() else { + throw FacebookProviderError + .configurationInvalid("Failed to create Facebook login configuration") + } + + let result = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation< + Void, + Error + >) in + loginManager.logIn( + configuration: configuration + ) { result in + switch result { + case .cancelled: + continuation + .resume(throwing: FacebookProviderError.signInCancelled("User cancelled sign-in")) + // showCanceledAlert = true + case let .failed(error): + continuation.resume(throwing: error) + // errorMessage = authService.string.localizedErrorMessage(for: error) + case .success: + continuation.resume() + } + } + } + if isLimitedLogin { + return try limitedLogin() + } else { + return try classicLogin() + } + } + + private func classicLogin() throws -> AuthCredential { + if let token = AccessToken.current, + !token.isExpired { + let credential = FacebookAuthProvider + .credential(withAccessToken: token.tokenString) + + return credential + } else { + throw FacebookProviderError + .accessToken( + "Access token has expired or not available. Please sign-in with Facebook before attempting to create a Facebook provider credential" + ) + } + } + + private func limitedLogin() throws -> AuthCredential { + if let idToken = AuthenticationToken.current { + let credential = OAuthProvider.credential(withProviderID: providerId, + idToken: idToken.tokenString, + rawNonce: rawNonce) + return credential + } else { + throw FacebookProviderError + .authenticationToken( + "Authentication is not available. Please sign-in with Facebook before attempting to create a Facebook provider credential" + ) + } + } +} diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift new file mode 100644 index 0000000000..1699a5bfb4 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift @@ -0,0 +1,110 @@ +import AppTrackingTransparency +import FacebookCore +import FacebookLogin +import FirebaseAuth +import FirebaseAuthSwiftUI +import SwiftUI + +@MainActor +public struct SignInWithFacebookButton { + @Environment(AuthService.self) private var authService + @State private var errorMessage = "" + @State private var showCanceledAlert = false + @State private var limitedLogin = true + @State private var showUserTrackingAlert = false + @State private var trackingAuthorizationStatus: ATTrackingManager + .AuthorizationStatus = .notDetermined + + public init() { + _trackingAuthorizationStatus = State(initialValue: ATTrackingManager + .trackingAuthorizationStatus) + } + + private var limitedLoginBinding: Binding { + Binding( + get: { self.limitedLogin }, + set: { newValue in + if trackingAuthorizationStatus == .authorized { + self.limitedLogin = newValue + } else { + self.limitedLogin = false + } + } + ) + } + + func requestTrackingPermission() { + ATTrackingManager.requestTrackingAuthorization { status in + Task { @MainActor in + trackingAuthorizationStatus = status + if status != .authorized { + showUserTrackingAlert = true + } + } + } + } +} + +extension SignInWithFacebookButton: View { + public var body: some View { + Button(action: { + Task { + do { + try await authService.signInWithFacebook(limitedLogin: limitedLogin) + } catch { + switch error { + case FacebookProviderError.signInCancelled: + showCanceledAlert = true + default: + errorMessage = authService.string.localizedErrorMessage(for: error) + } + } + } + }) { + HStack { + Image(systemName: "f.circle.fill") + .font(.title) + .foregroundColor(.white) + Text("Continue with Facebook") + .fontWeight(.semibold) + .foregroundColor(.white) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(8) + } + .alert(isPresented: $showCanceledAlert) { + Alert( + title: Text("Facebook login cancelled"), + dismissButton: .default(Text("OK")) + ) + } + + HStack { + Text("Authorize User Tracking") + .font(.footnote) + .foregroundColor(.blue) + .underline() + .onTapGesture { + requestTrackingPermission() + } + Toggle(isOn: limitedLoginBinding) { + HStack { + Spacer() // This will push the text to the left of the toggle + Text("Limited Login") + .foregroundColor(.blue) + } + } + .toggleStyle(SwitchToggleStyle(tint: .green)) + .alert(isPresented: $showUserTrackingAlert) { + Alert( + title: Text("Authorize User Tracking"), + message: Text("For classic Facebook login, please authorize user tracking."), + dismissButton: .default(Text("OK")) + ) + } + } + Text(errorMessage).foregroundColor(.red) + } +} diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Tests/FirebaseFacebookSwiftUITests/FirebaseFacebookSwiftUITests.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Tests/FirebaseFacebookSwiftUITests/FirebaseFacebookSwiftUITests.swift new file mode 100644 index 0000000000..0b2e3b1e3e --- /dev/null +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Tests/FirebaseFacebookSwiftUITests/FirebaseFacebookSwiftUITests.swift @@ -0,0 +1,6 @@ +@testable import FirebaseFacebookSwiftUI +import Testing + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/.gitignore b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/.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/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderSwift.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderSwift.swift new file mode 100644 index 0000000000..f042e95e71 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderSwift.swift @@ -0,0 +1,61 @@ +@preconcurrency import FirebaseAuth +import FirebaseAuthSwiftUI +import GoogleSignIn + +let kGoogleUserInfoEmailScope = "https://www.googleapis.com/auth/userinfo.email" +let kGoogleUserInfoProfileScope = "https://www.googleapis.com/auth/userinfo.profile" +let kDefaultScopes = [kGoogleUserInfoEmailScope, kGoogleUserInfoProfileScope] + +public enum GoogleProviderError: Error { + case rootViewControllerNotFound(String) + case authenticationToken(String) + case user(String) +} + +public class GoogleProviderSwift: @preconcurrency GoogleProviderProtocol { + let scopes: [String] + let shortName = "Google" + let providerId = "google.com" + public init(scopes: [String]? = nil) { + self.scopes = scopes ?? kDefaultScopes + } + + public func handleUrl(_ url: URL) -> Bool { + return GIDSignIn.sharedInstance.handle(url) + } + + @MainActor public func signInWithGoogle(clientID: String) async throws -> AuthCredential { + guard let presentingViewController = await (UIApplication.shared.connectedScenes + .first as? UIWindowScene)?.windows.first?.rootViewController else { + throw GoogleProviderError + .rootViewControllerNotFound( + "Root View controller is not available to present Google sign-in View." + ) + } + + let config = GIDConfiguration(clientID: clientID) + GIDSignIn.sharedInstance.configuration = config + + return try await withCheckedThrowingContinuation { continuation in + GIDSignIn.sharedInstance.signIn( + withPresenting: presentingViewController + ) { result, error in + if let error = error { + continuation.resume(throwing: error) + return + } + + guard let user = result?.user, + let idToken = user.idToken?.tokenString else { + continuation + .resume(throwing: GoogleProviderError.user("Failed to retrieve user or idToken.")) + return + } + + let credential = GoogleAuthProvider.credential(withIDToken: idToken, + accessToken: user.accessToken.tokenString) + continuation.resume(returning: credential) + } + } + } +} diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift new file mode 100644 index 0000000000..241b50ea48 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift @@ -0,0 +1,51 @@ +import FirebaseAuthSwiftUI +import SwiftUI + +@MainActor +public struct SignInWithGoogleButton { + @Environment(AuthService.self) private var authService + + public init() {} + + private func signInWithGoogle() async { + do { + try await authService.signInWithGoogle() + } catch {} + } +} + +extension SignInWithGoogleButton: View { + public var body: some View { + Button(action: { + Task { + try await signInWithGoogle() + } + }) { + if authService.authenticationState != .authenticating { + HStack { + Image(systemName: "globe") // Placeholder for Google logo + .resizable() + .frame(width: 20, height: 20) + .padding(.leading, 8) + + Text(authService + .authenticationFlow == .login ? "Login with Google" : "Sign-up with Google") + .foregroundColor(.black) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .background(Color.white) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray, lineWidth: 1) + ) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + } + } +} diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Tests/FirebaseGoogleSwiftUITests/FirebaseGoogleSwiftUITests.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Tests/FirebaseGoogleSwiftUITests/FirebaseGoogleSwiftUITests.swift new file mode 100644 index 0000000000..3f9a362a3a --- /dev/null +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Tests/FirebaseGoogleSwiftUITests/FirebaseGoogleSwiftUITests.swift @@ -0,0 +1,6 @@ +@testable import FirebaseGoogleSwiftUI +import Testing + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/.gitignore b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/.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/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderSwift.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderSwift.swift new file mode 100644 index 0000000000..df78c63ca8 --- /dev/null +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderSwift.swift @@ -0,0 +1,21 @@ +@preconcurrency import FirebaseAuth +import FirebaseAuthSwiftUI + +public typealias VerificationID = String + +public class PhoneAuthProviderSwift: @preconcurrency PhoneAuthProviderProtocol { + public init() {} + + @MainActor public func verifyPhoneNumber(phoneNumber: String) async throws -> VerificationID { + return try await withCheckedThrowingContinuation { continuation in + PhoneAuthProvider.provider() + .verifyPhoneNumber(phoneNumber, uiDelegate: nil) { verificationID, error in + if let error = error { + continuation.resume(throwing: error) + return + } + continuation.resume(returning: verificationID!) + } + } + } +} diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Utils/PhoneUtils.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Utils/PhoneUtils.swift new file mode 100644 index 0000000000..af3b939fea --- /dev/null +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Utils/PhoneUtils.swift @@ -0,0 +1,26 @@ +import Foundation + +class PhoneUtils { + static func isValidPhoneNumber(_ phoneNumber: String) -> Bool { + guard phoneNumber.first == "+" else { + return false + } + + let digits = phoneNumber.dropFirst() + guard !digits.isEmpty else { + return false + } + + guard digits.allSatisfy({ $0.isNumber }) else { + return false + } + + let minLength = 7 + let maxLength = 15 + guard digits.count >= minLength && digits.count <= maxLength else { + return false + } + + return true + } +} diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift new file mode 100644 index 0000000000..2addf55123 --- /dev/null +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift @@ -0,0 +1,92 @@ +import FirebaseAuthSwiftUI +import SwiftUI + +@MainActor +public struct PhoneAuthButtonView { + @Environment(AuthService.self) private var authService + @State private var errorMessage = "" + @State private var phoneNumber = "" + @State private var showVerificationCodeInput = false + @State private var verificationCode = "" + @State private var verificationID = "" + + public init() {} +} + +extension PhoneAuthButtonView: View { + public var body: some View { + if authService.authenticationState != .authenticating { + VStack { + LabeledContent { + TextField("Enter phone number", text: $phoneNumber) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .submitLabel(.next) + } label: { + Image(systemName: "at") + }.padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 4) + Button(action: { + Task { + do { + let id = try await authService.verifyPhoneNumber(phoneNumber: phoneNumber) + verificationID = id + showVerificationCodeInput = true + } catch { + errorMessage = authService.string.localizedErrorMessage( + for: error + ) + } + } + }) { + Text("Send SMS code") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .disabled(!PhoneUtils.isValidPhoneNumber(phoneNumber)) + .padding([.top, .bottom], 8) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + Text(errorMessage).foregroundColor(.red) + }.sheet(isPresented: $showVerificationCodeInput) { + TextField("Enter verification code", text: $verificationCode) + .keyboardType(.numberPad) + .padding() + .background(Color(.systemGray6)) + .cornerRadius(8) + .padding(.horizontal) + + Button(action: { + Task { + do { + try await authService.signInWithPhoneNumber( + verificationID: verificationID, + verificationCode: verificationCode + ) + } catch { + errorMessage = authService.string.localizedErrorMessage(for: error) + } + showVerificationCodeInput = false + } + }) { + Text("Verify phone number and sign-in") + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.green) + .cornerRadius(8) + .padding(.horizontal) + } + }.onOpenURL { url in + authService.auth.canHandle(url) + } + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + Text(errorMessage).foregroundColor(.red) + } +} diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Tests/FirebasePhoneAuthSwiftUITests/FirebasePhoneAuthSwiftUITests.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Tests/FirebasePhoneAuthSwiftUITests/FirebasePhoneAuthSwiftUITests.swift new file mode 100644 index 0000000000..983c1e8e8e --- /dev/null +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Tests/FirebasePhoneAuthSwiftUITests/FirebasePhoneAuthSwiftUITests.swift @@ -0,0 +1,6 @@ +@testable import FirebasePhoneAuthSwiftUI +import Testing + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/Package.resolved b/Package.resolved index 708a6697f7..4fcb81a8b3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,169 +1,168 @@ { - "object": { - "pins": [ - { - "package": "abseil", - "repositoryURL": "https://github.com/google/abseil-cpp-binary.git", - "state": { - "branch": null, - "revision": "194a6706acbd25e4ef639bcaddea16e8758a3e27", - "version": "1.2024011602.0" - } - }, - { - "package": "AppCheck", - "repositoryURL": "https://github.com/google/app-check.git", - "state": { - "branch": null, - "revision": "61b85103a1aeed8218f17c794687781505fbbef5", - "version": "11.2.0" - } - }, - { - "package": "AppAuth", - "repositoryURL": "https://github.com/openid/AppAuth-iOS.git", - "state": { - "branch": null, - "revision": "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2", - "version": "1.7.5" - } - }, - { - "package": "Facebook", - "repositoryURL": "https://github.com/facebook/facebook-ios-sdk.git", - "state": { - "branch": null, - "revision": "619d1772808425faa010d92293b26eeb9bc1d630", - "version": "17.4.0" - } - }, - { - "package": "Firebase", - "repositoryURL": "https://github.com/firebase/firebase-ios-sdk.git", - "state": { - "branch": null, - "revision": "dbdfdc44bee8b8e4eaa5ec27eb12b9338f3f2bc1", - "version": "11.5.0" - } - }, - { - "package": "GoogleAppMeasurement", - "repositoryURL": "https://github.com/google/GoogleAppMeasurement.git", - "state": { - "branch": null, - "revision": "4f234bcbdae841d7015258fbbf8e7743a39b8200", - "version": "11.4.0" - } - }, - { - "package": "GoogleDataTransport", - "repositoryURL": "https://github.com/google/GoogleDataTransport.git", - "state": { - "branch": null, - "revision": "617af071af9aa1d6a091d59a202910ac482128f9", - "version": "10.1.0" - } - }, - { - "package": "GoogleSignIn", - "repositoryURL": "https://github.com/google/GoogleSignIn-iOS", - "state": { - "branch": null, - "revision": "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version": "7.1.0" - } - }, - { - "package": "GoogleUtilities", - "repositoryURL": "https://github.com/google/GoogleUtilities.git", - "state": { - "branch": null, - "revision": "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", - "version": "8.0.2" - } - }, - { - "package": "gRPC", - "repositoryURL": "https://github.com/google/grpc-binary.git", - "state": { - "branch": null, - "revision": "f56d8fc3162de9a498377c7b6cea43431f4f5083", - "version": "1.65.1" - } - }, - { - "package": "GTMSessionFetcher", - "repositoryURL": "https://github.com/google/gtm-session-fetcher.git", - "state": { - "branch": null, - "revision": "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version": "3.5.0" - } - }, - { - "package": "GTMAppAuth", - "repositoryURL": "https://github.com/google/GTMAppAuth.git", - "state": { - "branch": null, - "revision": "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version": "4.1.1" - } - }, - { - "package": "InteropForGoogle", - "repositoryURL": "https://github.com/google/interop-ios-for-google-sdks.git", - "state": { - "branch": null, - "revision": "2d12673670417654f08f5f90fdd62926dc3a2648", - "version": "100.0.0" - } - }, - { - "package": "leveldb", - "repositoryURL": "https://github.com/firebase/leveldb.git", - "state": { - "branch": null, - "revision": "a0bc79961d7be727d258d33d5a6b2f1023270ba1", - "version": "1.22.5" - } - }, - { - "package": "nanopb", - "repositoryURL": "https://github.com/firebase/nanopb.git", - "state": { - "branch": null, - "revision": "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version": "2.30910.0" - } - }, - { - "package": "Promises", - "repositoryURL": "https://github.com/google/promises.git", - "state": { - "branch": null, - "revision": "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version": "2.4.0" - } - }, - { - "package": "SDWebImage", - "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", - "state": { - "branch": null, - "revision": "10d06f6a33bafae8c164fbfd1f03391f6d4692b3", - "version": "5.20.0" - } - }, - { - "package": "SwiftProtobuf", - "repositoryURL": "https://github.com/apple/swift-protobuf.git", - "state": { - "branch": null, - "revision": "ebc7251dd5b37f627c93698e4374084d98409633", - "version": "1.28.2" - } - } - ] - }, - "version": 1 + "originHash" : "34be7fa97c361a1f569f13c56a65c12d69c8f509bedf7f43bed90e477b69a9bc", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" + } + }, + { + "identity" : "facebook-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/facebook/facebook-ios-sdk.git", + "state" : { + "revision" : "619d1772808425faa010d92293b26eeb9bc1d630", + "version" : "17.4.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk.git", + "state" : { + "revision" : "d1f7c7e8eaa74d7e44467184dc5f592268247d33", + "version" : "11.11.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "dd89fc79a77183830742a16866d87e4e54785734", + "version" : "11.11.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS", + "state" : { + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", + "version" : "1.69.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "cac9a55a3ae92478a2c95042dcc8d9695d2129ca", + "version" : "5.21.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" + } + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index 937282f068..a629a986fb 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ import PackageDescription let package = Package( name: "FirebaseUI", defaultLocalization: "en", - platforms: [.iOS(.v13)], + platforms: [.iOS(.v17)], products: [ .library( name: "FirebaseAnonymousAuthUI", @@ -62,17 +62,33 @@ let package = Package( name: "FirebaseStorageUI", targets: ["FirebaseStorageUI"] ), + .library( + name: "FirebaseAuthSwiftUI", + targets: ["FirebaseAuthSwiftUI"] + ), + .library( + name: "FirebaseGoogleSwiftUI", + targets: ["FirebaseGoogleSwiftUI"] + ), + .library( + name: "FirebaseFacebookSwiftUI", + targets: ["FirebaseFacebookSwiftUI"] + ), + .library( + name: "FirebasePhoneAuthSwiftUI", + targets: ["FirebasePhoneAuthSwiftUI"] + ), ], dependencies: [ .package( - name: "Facebook", + name: "Facebook", url: "https://github.com/facebook/facebook-ios-sdk.git", - "17.0.0"..<"18.0.0" + "17.0.0" ..< "18.0.0" ), .package( - name: "Firebase", + name: "Firebase", url: "https://github.com/firebase/firebase-ios-sdk.git", - "8.0.0"..<"12.0.0" + "8.0.0" ..< "12.0.0" ), .package( name: "GoogleSignIn", @@ -82,7 +98,7 @@ let package = Package( .package( name: "GoogleUtilities", url: "https://github.com/google/GoogleUtilities.git", - "7.4.1"..<"9.0.0" + "7.4.1" ..< "9.0.0" ), .package( name: "SDWebImage", @@ -183,7 +199,7 @@ let package = Package( name: "FirebaseGoogleAuthUI", dependencies: [ "FirebaseAuthUI", - "GoogleSignIn" + "GoogleSignIn", ], path: "FirebaseGoogleAuthUI/Sources", exclude: ["Info.plist"], @@ -241,5 +257,59 @@ let package = Package( .headerSearchPath("../../"), ] ), + .target( + name: "FirebaseAuthSwiftUI", + dependencies: [ + .product(name: "FirebaseAuth", package: "Firebase"), + ], + path: "FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources", + resources: [ + .process("Strings"), + ] + ), + .testTarget( + name: "FirebaseAuthSwiftUITests", + dependencies: ["FirebaseAuthSwiftUI"], + path: "FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/" + ), + .target( + name: "FirebaseGoogleSwiftUI", + dependencies: [ + "FirebaseAuthSwiftUI", + "GoogleSignIn", + ], + path: "FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources" + ), + .testTarget( + name: "FirebaseGoogleSwiftUITests", + dependencies: ["FirebaseGoogleSwiftUI"], + path: "FirebaseSwiftUI/FirebaseGoogleSwiftUI/Tests/" + ), + .target( + name: "FirebaseFacebookSwiftUI", + dependencies: [ + "FirebaseAuthSwiftUI", + .product(name: "FacebookLogin", package: "Facebook"), + .product(name: "FacebookCore", package: "Facebook"), + ], + path: "FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources" + ), + .testTarget( + name: "FirebaseFacebookSwiftUITests", + dependencies: ["FirebaseFacebookSwiftUI"], + path: "FirebaseSwiftUI/FirebaseFacebookSwiftUI/Tests/" + ), + .target( + name: "FirebasePhoneAuthSwiftUI", + dependencies: [ + "FirebaseAuthSwiftUI", + ], + path: "FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources" + ), + .testTarget( + name: "FirebasePhoneAuthSwiftUITests", + dependencies: ["FirebasePhoneAuthSwiftUI"], + path: "FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Tests/" + ), ] ) diff --git a/format-swift.sh b/format-swift.sh new file mode 100755 index 0000000000..0aec197f4a --- /dev/null +++ b/format-swift.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +swiftformat ./FirebaseSwiftUI + +swiftformat ./samples/swiftui/FirebaseSwiftUIExample + + +swiftformat ./Package.swift \ No newline at end of file diff --git a/samples/swift/FirebaseUI-demo-swift.xcodeproj/project.pbxproj b/samples/swift/FirebaseUI-demo-swift.xcodeproj/project.pbxproj index 476e1ed983..83f23d6129 100644 --- a/samples/swift/FirebaseUI-demo-swift.xcodeproj/project.pbxproj +++ b/samples/swift/FirebaseUI-demo-swift.xcodeproj/project.pbxproj @@ -36,6 +36,8 @@ C39BC0511DB812330060F6AF /* FUICustomPasswordVerificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C39BC04F1DB812330060F6AF /* FUICustomPasswordVerificationViewController.swift */; }; C39BC0521DB812330060F6AF /* FUICustomPasswordVerificationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C39BC0501DB812330060F6AF /* FUICustomPasswordVerificationViewController.xib */; }; C3F23ECD1D80F3300020509F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C3F23ECC1D80F3300020509F /* GoogleService-Info.plist */; }; + D7FF20C65AA51946FA2E77C4 /* Pods_FirebaseUI_demo_swiftTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1B266F262E24512C267E455 /* Pods_FirebaseUI_demo_swiftTests.framework */; }; + DB34EAFE145DDD05F2A53E21 /* Pods_FirebaseUI_demo_swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6651780F4BD5F5177BD9E6D /* Pods_FirebaseUI_demo_swift.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,6 +51,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1DF85F38940228B710FA2D36 /* Pods-FirebaseUI-demo-swiftTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseUI-demo-swiftTests.release.xcconfig"; path = "Target Support Files/Pods-FirebaseUI-demo-swiftTests/Pods-FirebaseUI-demo-swiftTests.release.xcconfig"; sourceTree = ""; }; + 23633FE989279E7D3A893CB5 /* Pods-FirebaseUI-demo-swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseUI-demo-swift.release.xcconfig"; path = "Target Support Files/Pods-FirebaseUI-demo-swift/Pods-FirebaseUI-demo-swift.release.xcconfig"; sourceTree = ""; }; 89B2924622568B1C00CEF7D7 /* twtrsymbol.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = twtrsymbol.png; sourceTree = ""; }; 8D5F93AF1D9B192D00D5A2E4 /* StorageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageViewController.swift; sourceTree = ""; }; 8DABC9851D3D82D600453807 /* FirebaseUI-demo-swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FirebaseUI-demo-swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -59,6 +63,9 @@ 8DABC99D1D3D82D600453807 /* FirebaseUI-demo-swiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FirebaseUI-demo-swiftTests.swift"; sourceTree = ""; }; 8DABC99F1D3D82D600453807 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8DD51E361D873B0D00E2CA51 /* UIStoryboardExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIStoryboardExtension.swift; sourceTree = ""; }; + A6651780F4BD5F5177BD9E6D /* Pods_FirebaseUI_demo_swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FirebaseUI_demo_swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B1B266F262E24512C267E455 /* Pods_FirebaseUI_demo_swiftTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FirebaseUI_demo_swiftTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BF8F7C7E13D6D81A1E57351C /* Pods-FirebaseUI-demo-swiftTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseUI-demo-swiftTests.debug.xcconfig"; path = "Target Support Files/Pods-FirebaseUI-demo-swiftTests/Pods-FirebaseUI-demo-swiftTests.debug.xcconfig"; sourceTree = ""; }; C302C1D51D91CC7B00ADBD41 /* FUIAuthViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUIAuthViewController.swift; sourceTree = ""; }; C302C1D71D91CC7B00ADBD41 /* ChatCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewCell.swift; sourceTree = ""; }; C302C1D81D91CC7B00ADBD41 /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; @@ -166,6 +173,7 @@ C39BC04F1DB812330060F6AF /* FUICustomPasswordVerificationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUICustomPasswordVerificationViewController.swift; sourceTree = ""; }; C39BC0501DB812330060F6AF /* FUICustomPasswordVerificationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FUICustomPasswordVerificationViewController.xib; sourceTree = ""; }; C3F23ECC1D80F3300020509F /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + D1B02880ECAE7C8AB98C6332 /* Pods-FirebaseUI-demo-swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseUI-demo-swift.debug.xcconfig"; path = "Target Support Files/Pods-FirebaseUI-demo-swift/Pods-FirebaseUI-demo-swift.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -173,6 +181,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DB34EAFE145DDD05F2A53E21 /* Pods_FirebaseUI_demo_swift.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -180,12 +189,22 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D7FF20C65AA51946FA2E77C4 /* Pods_FirebaseUI_demo_swiftTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 77499FA4EF7D4F9C7DE36B95 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A6651780F4BD5F5177BD9E6D /* Pods_FirebaseUI_demo_swift.framework */, + B1B266F262E24512C267E455 /* Pods_FirebaseUI_demo_swiftTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 8D5F93B11D9B193C00D5A2E4 /* Storage */ = { isa = PBXGroup; children = ( @@ -202,6 +221,7 @@ 8DABC99C1D3D82D600453807 /* FirebaseUI-demo-swiftTests */, 8DABC9861D3D82D600453807 /* Products */, 9C43BF8CA810E7C909775084 /* Pods */, + 77499FA4EF7D4F9C7DE36B95 /* Frameworks */, ); sourceTree = ""; }; @@ -240,6 +260,10 @@ 9C43BF8CA810E7C909775084 /* Pods */ = { isa = PBXGroup; children = ( + D1B02880ECAE7C8AB98C6332 /* Pods-FirebaseUI-demo-swift.debug.xcconfig */, + 23633FE989279E7D3A893CB5 /* Pods-FirebaseUI-demo-swift.release.xcconfig */, + BF8F7C7E13D6D81A1E57351C /* Pods-FirebaseUI-demo-swiftTests.debug.xcconfig */, + 1DF85F38940228B710FA2D36 /* Pods-FirebaseUI-demo-swiftTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -305,9 +329,11 @@ isa = PBXNativeTarget; buildConfigurationList = 8DABC9A21D3D82D600453807 /* Build configuration list for PBXNativeTarget "FirebaseUI-demo-swift" */; buildPhases = ( + A711A34CF80CD0C969A9E286 /* [CP] Check Pods Manifest.lock */, 8DABC9811D3D82D600453807 /* Sources */, 8DABC9821D3D82D600453807 /* Frameworks */, 8DABC9831D3D82D600453807 /* Resources */, + 53A915D8CB2EA7687E61107D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -322,6 +348,7 @@ isa = PBXNativeTarget; buildConfigurationList = 8DABC9A51D3D82D600453807 /* Build configuration list for PBXNativeTarget "FirebaseUI-demo-swiftTests" */; buildPhases = ( + 665BFB596CE5C127F3D5D0E5 /* [CP] Check Pods Manifest.lock */, 8DABC9951D3D82D600453807 /* Sources */, 8DABC9961D3D82D600453807 /* Frameworks */, 8DABC9971D3D82D600453807 /* Resources */, @@ -348,6 +375,7 @@ TargetAttributes = { 8DABC9841D3D82D600453807 = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = YYX2P3XVJ7; LastSwiftMigration = 1020; SystemCapabilities = { com.apple.BackgroundModes = { @@ -496,6 +524,149 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 53A915D8CB2EA7687E61107D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FirebaseUI-demo-swift/Pods-FirebaseUI-demo-swift-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/AppAuth/AppAuth.framework", + "${BUILT_PRODUCTS_DIR}/AppCheckCore/AppCheckCore.framework", + "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC/openssl_grpc.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAnonymousAuthUI/FirebaseAnonymousAuthUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAppCheckInterop/FirebaseAppCheckInterop.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAuth/FirebaseAuth.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAuthInterop/FirebaseAuthInterop.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAuthUI/FirebaseAuthUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCoreExtension/FirebaseCoreExtension.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseDatabase/FirebaseDatabase.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseDatabaseUI/FirebaseDatabaseUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseEmailAuthUI/FirebaseEmailAuthUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseFacebookAuthUI/FirebaseFacebookAuthUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseFirestore/FirebaseFirestore.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseFirestoreInternal/FirebaseFirestoreInternal.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseFirestoreUI/FirebaseFirestoreUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseGoogleAuthUI/FirebaseGoogleAuthUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseOAuthUI/FirebaseOAuthUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebasePhoneAuthUI/FirebasePhoneAuthUI.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseSharedSwift/FirebaseSharedSwift.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseStorage/FirebaseStorage.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseStorageUI/FirebaseStorageUI.framework", + "${BUILT_PRODUCTS_DIR}/GTMAppAuth/GTMAppAuth.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleSignIn/GoogleSignIn.framework", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", + "${BUILT_PRODUCTS_DIR}/RecaptchaInterop/RecaptchaInterop.framework", + "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", + "${BUILT_PRODUCTS_DIR}/abseil/absl.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-C++/grpcpp.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FBAEMKit/FBAEMKit.framework/FBAEMKit", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FBSDKCoreKit/FBSDKCoreKit.framework/FBSDKCoreKit", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FBSDKCoreKit_Basics/FBSDKCoreKit_Basics.framework/FBSDKCoreKit_Basics", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FBSDKLoginKit/FBSDKLoginKit.framework/FBSDKLoginKit", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppAuth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppCheckCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl_grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAnonymousAuthUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAppCheckInterop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAuth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAuthInterop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAuthUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreExtension.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseDatabase.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseDatabaseUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseEmailAuthUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFacebookAuthUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFirestore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFirestoreInternal.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFirestoreUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseGoogleAuthUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseOAuthUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebasePhoneAuthUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseSharedSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseStorage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseStorageUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMAppAuth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleSignIn.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RecaptchaInterop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/absl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpcpp.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBAEMKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBSDKCoreKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBSDKCoreKit_Basics.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBSDKLoginKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FirebaseUI-demo-swift/Pods-FirebaseUI-demo-swift-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 665BFB596CE5C127F3D5D0E5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FirebaseUI-demo-swiftTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A711A34CF80CD0C969A9E286 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FirebaseUI-demo-swift-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 8DABC9811D3D82D600453807 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -747,12 +918,13 @@ }; 8DABC9A31D3D82D600453807 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D1B02880ECAE7C8AB98C6332 /* Pods-FirebaseUI-demo-swift.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BITCODE_GENERATION_MODE = ""; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "FirebaseUI-demo-swift/FirebaseUI-demo-swift.entitlements"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = YYX2P3XVJ7; HEADER_SEARCH_PATHS = ( "$(inherited)", "${PODS_ROOT}/Firebase/Core/Sources", @@ -776,7 +948,7 @@ INFOPLIST_FILE = "FirebaseUI-demo-swift/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "com.google.firebase.firebaseui.FirebaseUI-demo-swift"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.auth.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -786,12 +958,13 @@ }; 8DABC9A41D3D82D600453807 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 23633FE989279E7D3A893CB5 /* Pods-FirebaseUI-demo-swift.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BITCODE_GENERATION_MODE = ""; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "FirebaseUI-demo-swift/FirebaseUI-demo-swift.entitlements"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = YYX2P3XVJ7; HEADER_SEARCH_PATHS = ( "$(inherited)", "${PODS_ROOT}/Firebase/Core/Sources", @@ -815,7 +988,7 @@ INFOPLIST_FILE = "FirebaseUI-demo-swift/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "com.google.firebase.firebaseui.FirebaseUI-demo-swift"; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.auth.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 6.0; @@ -824,6 +997,7 @@ }; 8DABC9A61D3D82D600453807 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BF8F7C7E13D6D81A1E57351C /* Pods-FirebaseUI-demo-swiftTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = "FirebaseUI-demo-swiftTests/Info.plist"; @@ -837,6 +1011,7 @@ }; 8DABC9A71D3D82D600453807 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1DF85F38940228B710FA2D36 /* Pods-FirebaseUI-demo-swiftTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = "FirebaseUI-demo-swiftTests/Info.plist"; diff --git a/samples/swift/FirebaseUI-demo-swift/Info.plist b/samples/swift/FirebaseUI-demo-swift/Info.plist index 18bb33403a..99fb722e04 100644 --- a/samples/swift/FirebaseUI-demo-swift/Info.plist +++ b/samples/swift/FirebaseUI-demo-swift/Info.plist @@ -2,8 +2,6 @@ - FacebookClientToken - aaaaa CFBundleDevelopmentRegion en CFBundleExecutable @@ -47,6 +45,8 @@ 1 FacebookAppID {your-app-id} + FacebookClientToken + aaaaa FacebookDisplayName {your-app-name} LSApplicationQueriesSchemes diff --git a/samples/swift/Podfile.lock b/samples/swift/Podfile.lock index fa987e48dc..b3c7c94427 100644 --- a/samples/swift/Podfile.lock +++ b/samples/swift/Podfile.lock @@ -1548,26 +1548,26 @@ SPEC CHECKSUMS: FBSDKCoreKit: 94d7461d0cecf441b1ba7c41acfff41daa8ccd41 FBSDKCoreKit_Basics: 151b43db8b834d3f0e02f95d36a44ffd36265e45 FBSDKLoginKit: 5c1cd53c91a2282b3a4fe6e6d3dcf2b8b0d33d55 - FirebaseAnonymousAuthUI: beac8a00da8c765f14e01bfcd13d9fde8b991e1f - FirebaseAppCheckInterop: 9226f7217b43e99dfa0bc9f674ad8108cef89feb - FirebaseAuth: 8eebb43f35cd2c1bb1981e3cef27c45617b85316 - FirebaseAuthInterop: 2a26ee1bea6d47df8048683cfa071e7da657798f - FirebaseAuthUI: d2c84c671a24b9d669b4d2483cfd261c2c6e5bab - FirebaseCore: 7cfcf7e8b33985c06478fd2b96e2f7101eb61a1c - FirebaseCoreExtension: b35fed507db40e6224fe36b163d34d18147da368 - FirebaseCoreInternal: 154779013b85b4b290fdba38414df475f42e3096 - FirebaseDatabase: 3753a5df354b0557ba96da9cdfead7900fb65d92 + FirebaseAnonymousAuthUI: eb2d9ea96dc1121f47130619abc93a290479bbc1 + FirebaseAppCheckInterop: 2376d3ec5cb4267facad4fe754ab4f301a5a519b + FirebaseAuth: 77e25aa24f3e1c626c5babd3338551fc1669ee0e + FirebaseAuthInterop: a6973d72aa242ea88ffb6be9c9b06c65455071da + FirebaseAuthUI: 29804b552165df676c3882669b5ef99717696fa8 + FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4 + FirebaseCoreExtension: 206c1b399f0d103055207c16f299b28e3dbd1949 + FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881 + FirebaseDatabase: b014c0068fa691dec5f4a868357e685df882cca5 FirebaseDatabaseUI: 6b629132ecc79e624b76f5b370921eea30c06503 - FirebaseEmailAuthUI: f81d35790f1647484af81fb8b96ee81bc3eb9fe7 - FirebaseFacebookAuthUI: 65d7e20aef4d9107fc3edf9274f144cde706fecd - FirebaseFirestore: b6d8e413b6e3ebc5e2d0beac2371c6aefb740ef3 - FirebaseFirestoreInternal: 259eb0771fdf24d03d86d8f9692dda6df75cac28 + FirebaseEmailAuthUI: 52589b1e800fa0f64983d6a5b7638fd24c3b5d7a + FirebaseFacebookAuthUI: 47c2e83010d9b2dacf55880ac7cd9d937c5cf35b + FirebaseFirestore: e8528981558d50872e24119b9448b18e86cab9f6 + FirebaseFirestoreInternal: 953e31e70db09d928b927b65761171c680d2ea9b FirebaseFirestoreUI: 5e367973b79caec884a4e16269c97f991ddaf5de - FirebaseGoogleAuthUI: 11ead69cc92ac3f86f94692d0e96cabe22f61121 + FirebaseGoogleAuthUI: c4dd51dbfa267af8d3eda87b35ffb01eb9b23062 FirebaseOAuthUI: 784f4f756adebbfaae892a873c0b98085b2f12d6 - FirebasePhoneAuthUI: 9f80bfb5fa883ea5a0a33ebad25f28c2ac8111e7 - FirebaseSharedSwift: 574e6a5602afe4397a55c8d4f767382d620285de - FirebaseStorage: ef4654f6c54b702deb4d74650dde22c7b8ff0d60 + FirebasePhoneAuthUI: d8266993e3ff28bc9daf423e29325993f951fecc + FirebaseSharedSwift: a45efd84d60ebbfdcdbaebc66948af3630459e62 + FirebaseStorage: d35da127dd49edcbd07b8c07cf651a70161558b2 FirebaseStorageUI: 646d8e65d699ba733c16b66e7fd2fae4d33b4b78 FirebaseUI: 0d38fa4fda5606b3da0af78a829c960f3a81ca50 GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 @@ -1584,4 +1584,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 2c24841f482dcea2f8eb8b3a4c3bfeda6423a0b3 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..390e704b85 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj @@ -0,0 +1,656 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 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 */; }; + 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 */; }; + 46F89C4D2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 46F89C4C2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 46F89C1B2D64A86D000F8BC0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 46F89C002D64A86C000F8BC0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 46F89C072D64A86C000F8BC0; + remoteInfo = FirebaseSwiftUIExample; + }; + 46F89C252D64A86D000F8BC0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 46F89C002D64A86C000F8BC0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 46F89C072D64A86C000F8BC0; + remoteInfo = FirebaseSwiftUIExample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 46CB7B242D773F2100F1FD0A /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 46F89C082D64A86C000F8BC0 /* FirebaseSwiftUIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FirebaseSwiftUIExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 46F89C1A2D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FirebaseSwiftUIExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 46F89C242D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FirebaseSwiftUIExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 4607CC8A2D9BE8D9009EC3F5 /* Exceptions for "FirebaseSwiftUIExample" folder in "FirebaseSwiftUIExample" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 46F89C072D64A86C000F8BC0 /* FirebaseSwiftUIExample */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 46F89C0A2D64A86C000F8BC0 /* FirebaseSwiftUIExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4607CC8A2D9BE8D9009EC3F5 /* Exceptions for "FirebaseSwiftUIExample" folder in "FirebaseSwiftUIExample" target */, + ); + path = FirebaseSwiftUIExample; + sourceTree = ""; + }; + 46F89C1D2D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = FirebaseSwiftUIExampleTests; + sourceTree = ""; + }; + 46F89C272D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = FirebaseSwiftUIExampleUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 46F89C052D64A86C000F8BC0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4670DEA72D9EA9E100E0D36A /* FirebaseFacebookSwiftUI in Frameworks */, + 46F89C392D64B04E000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */, + 46F89C4D2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */, + 4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */, + 46C4EAB32DA801B200FC878B /* FirebasePhoneAuthSwiftUI in Frameworks */, + 4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 46F89C172D64A86D000F8BC0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 46F89C212D64A86D000F8BC0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 46A94CD12D7ECB4800F872B2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 46F89BFF2D64A86C000F8BC0 = { + isa = PBXGroup; + children = ( + 46CB7B242D773F2100F1FD0A /* GoogleService-Info.plist */, + 46F89C0A2D64A86C000F8BC0 /* FirebaseSwiftUIExample */, + 46F89C1D2D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests */, + 46F89C272D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests */, + 46A94CD12D7ECB4800F872B2 /* Frameworks */, + 46F89C092D64A86C000F8BC0 /* Products */, + ); + sourceTree = ""; + }; + 46F89C092D64A86C000F8BC0 /* Products */ = { + isa = PBXGroup; + children = ( + 46F89C082D64A86C000F8BC0 /* FirebaseSwiftUIExample.app */, + 46F89C1A2D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests.xctest */, + 46F89C242D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 46F89C072D64A86C000F8BC0 /* FirebaseSwiftUIExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 46F89C2E2D64A86D000F8BC0 /* Build configuration list for PBXNativeTarget "FirebaseSwiftUIExample" */; + buildPhases = ( + 46F89C042D64A86C000F8BC0 /* Sources */, + 46F89C052D64A86C000F8BC0 /* Frameworks */, + 46F89C062D64A86C000F8BC0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 46F89C0A2D64A86C000F8BC0 /* FirebaseSwiftUIExample */, + ); + name = FirebaseSwiftUIExample; + packageProductDependencies = ( + 46F89C382D64B04E000F8BC0 /* FirebaseAuthSwiftUI */, + 46F89C4C2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI */, + 4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */, + 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */, + 4670DEA62D9EA9E100E0D36A /* FirebaseFacebookSwiftUI */, + 46C4EAB22DA801B200FC878B /* FirebasePhoneAuthSwiftUI */, + ); + productName = FirebaseSwiftUIExample; + productReference = 46F89C082D64A86C000F8BC0 /* FirebaseSwiftUIExample.app */; + productType = "com.apple.product-type.application"; + }; + 46F89C192D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 46F89C312D64A86D000F8BC0 /* Build configuration list for PBXNativeTarget "FirebaseSwiftUIExampleTests" */; + buildPhases = ( + 46F89C162D64A86D000F8BC0 /* Sources */, + 46F89C172D64A86D000F8BC0 /* Frameworks */, + 46F89C182D64A86D000F8BC0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 46F89C1C2D64A86D000F8BC0 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 46F89C1D2D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests */, + ); + name = FirebaseSwiftUIExampleTests; + packageProductDependencies = ( + ); + productName = FirebaseSwiftUIExampleTests; + productReference = 46F89C1A2D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 46F89C232D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 46F89C342D64A86D000F8BC0 /* Build configuration list for PBXNativeTarget "FirebaseSwiftUIExampleUITests" */; + buildPhases = ( + 46F89C202D64A86D000F8BC0 /* Sources */, + 46F89C212D64A86D000F8BC0 /* Frameworks */, + 46F89C222D64A86D000F8BC0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 46F89C262D64A86D000F8BC0 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 46F89C272D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests */, + ); + name = FirebaseSwiftUIExampleUITests; + packageProductDependencies = ( + ); + productName = FirebaseSwiftUIExampleUITests; + productReference = 46F89C242D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 46F89C002D64A86C000F8BC0 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 46F89C072D64A86C000F8BC0 = { + CreatedOnToolsVersion = 16.2; + }; + 46F89C192D64A86D000F8BC0 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 46F89C072D64A86C000F8BC0; + }; + 46F89C232D64A86D000F8BC0 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 46F89C072D64A86C000F8BC0; + }; + }; + }; + buildConfigurationList = 46F89C032D64A86C000F8BC0 /* Build configuration list for PBXProject "FirebaseSwiftUIExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 46F89BFF2D64A86C000F8BC0; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 4607CC9A2D9BFE29009EC3F5 /* XCLocalSwiftPackageReference "../../../../firebaseUI-ios" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 46F89C092D64A86C000F8BC0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 46F89C072D64A86C000F8BC0 /* FirebaseSwiftUIExample */, + 46F89C192D64A86D000F8BC0 /* FirebaseSwiftUIExampleTests */, + 46F89C232D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 46F89C062D64A86C000F8BC0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 46CB7B252D773F2100F1FD0A /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 46F89C182D64A86D000F8BC0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 46F89C222D64A86D000F8BC0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 46F89C042D64A86C000F8BC0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 46F89C162D64A86D000F8BC0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 46F89C202D64A86D000F8BC0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 46F89C1C2D64A86D000F8BC0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 46F89C072D64A86C000F8BC0 /* FirebaseSwiftUIExample */; + targetProxy = 46F89C1B2D64A86D000F8BC0 /* PBXContainerItemProxy */; + }; + 46F89C262D64A86D000F8BC0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 46F89C072D64A86C000F8BC0 /* FirebaseSwiftUIExample */; + targetProxy = 46F89C252D64A86D000F8BC0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 46F89C2C2D64A86D000F8BC0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 46F89C2D2D64A86D000F8BC0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 46F89C2F2D64A86D000F8BC0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FirebaseSwiftUIExample/Preview Content\""; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = FirebaseSwiftUIExample/Info.plist; + INFOPLIST_KEY_NSUserTrackingUsageDescription = "Testing - need to authorize User tracking for Facebook classic login."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.auth.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 46F89C302D64A86D000F8BC0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FirebaseSwiftUIExample/Preview Content\""; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = FirebaseSwiftUIExample/Info.plist; + INFOPLIST_KEY_NSUserTrackingUsageDescription = "Testing - need to authorize User tracking for Facebook classic login."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.auth.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 46F89C322D64A86D000F8BC0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing.FirebaseSwiftUIExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FirebaseSwiftUIExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FirebaseSwiftUIExample"; + }; + name = Debug; + }; + 46F89C332D64A86D000F8BC0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing.FirebaseSwiftUIExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FirebaseSwiftUIExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FirebaseSwiftUIExample"; + }; + name = Release; + }; + 46F89C352D64A86D000F8BC0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing.FirebaseSwiftUIExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = FirebaseSwiftUIExample; + }; + name = Debug; + }; + 46F89C362D64A86D000F8BC0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing.FirebaseSwiftUIExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = FirebaseSwiftUIExample; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 46F89C032D64A86C000F8BC0 /* Build configuration list for PBXProject "FirebaseSwiftUIExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 46F89C2C2D64A86D000F8BC0 /* Debug */, + 46F89C2D2D64A86D000F8BC0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 46F89C2E2D64A86D000F8BC0 /* Build configuration list for PBXNativeTarget "FirebaseSwiftUIExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 46F89C2F2D64A86D000F8BC0 /* Debug */, + 46F89C302D64A86D000F8BC0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 46F89C312D64A86D000F8BC0 /* Build configuration list for PBXNativeTarget "FirebaseSwiftUIExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 46F89C322D64A86D000F8BC0 /* Debug */, + 46F89C332D64A86D000F8BC0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 46F89C342D64A86D000F8BC0 /* Build configuration list for PBXNativeTarget "FirebaseSwiftUIExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 46F89C352D64A86D000F8BC0 /* Debug */, + 46F89C362D64A86D000F8BC0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 4607CC9A2D9BFE29009EC3F5 /* XCLocalSwiftPackageReference "../../../../firebaseUI-ios" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../../firebaseUI-ios"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAuthSwiftUI; + }; + 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseGoogleSwiftUI; + }; + 4670DEA62D9EA9E100E0D36A /* FirebaseFacebookSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = 4607CC9A2D9BFE29009EC3F5 /* XCLocalSwiftPackageReference "../../../../firebaseUI-ios" */; + productName = FirebaseFacebookSwiftUI; + }; + 46C4EAB22DA801B200FC878B /* FirebasePhoneAuthSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = 4607CC9A2D9BFE29009EC3F5 /* XCLocalSwiftPackageReference "../../../../firebaseUI-ios" */; + productName = FirebasePhoneAuthSwiftUI; + }; + 46F89C382D64B04E000F8BC0 /* FirebaseAuthSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAuthSwiftUI; + }; + 46F89C4C2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAuthSwiftUI; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 46F89C002D64A86C000F8BC0 /* Project object */; +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExample.xcscheme b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExample.xcscheme new file mode 100644 index 0000000000..30faacec20 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExample.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/Contents.json b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements new file mode 100644 index 0000000000..ea83d33fa9 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:flutterfire-e2e-tests.firebaseapp.com + + + diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift new file mode 100644 index 0000000000..c5f85d500f --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift @@ -0,0 +1,110 @@ +// +// FirebaseSwiftUIExampleApp.swift +// FirebaseSwiftUIExample +// +// Created by Russell Wheatley on 18/02/2025. +// + +import FacebookCore +import FirebaseAuth +import FirebaseAuthSwiftUI +import FirebaseCore +import FirebaseFacebookSwiftUI +import FirebaseGoogleSwiftUI +import FirebasePhoneAuthSwiftUI +import SwiftData +import SwiftUI + +let googleProvider = GoogleProviderSwift() + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [ + UIApplication.LaunchOptionsKey: Any + ]?) -> Bool { + FirebaseApp.configure() + ApplicationDelegate.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) + return true + } + + func application(_: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + Auth.auth().setAPNSToken(deviceToken, type: .prod) + } + + func application(_: UIApplication, didReceiveRemoteNotification notification: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) + -> Void) { + if Auth.auth().canHandleNotification(notification) { + completionHandler(.noData) + return + } + } + + func application(_ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + ApplicationDelegate.shared.application( + app, + open: url, + sourceApplication: options[UIApplication.OpenURLOptionsKey + .sourceApplication] as? String, + annotation: options[UIApplication.OpenURLOptionsKey.annotation] + ) + + return googleProvider.handleUrl(url) + } +} + +@main +struct FirebaseSwiftUIExampleApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + init() {} + + var body: some Scene { + WindowGroup { + NavigationView { + ContentView() + } + } + } +} + +struct ContentView: View { + let authService: AuthService + + init() { + // Auth.auth().signInAnonymously() + + let actionCodeSettings = ActionCodeSettings() + actionCodeSettings.handleCodeInApp = true + actionCodeSettings + .url = URL(string: "https://flutterfire-e2e-tests.firebaseapp.com") + actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com" + actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!) + let configuration = AuthConfiguration( + shouldAutoUpgradeAnonymousUsers: true, + emailLinkSignInActionCodeSettings: actionCodeSettings + ) + let facebookProvider = FacebookProviderSwift() + let phoneAuthProvider = PhoneAuthProviderSwift() + authService = AuthService( + configuration: configuration, + googleProvider: googleProvider, + facebookProvider: facebookProvider, + phoneAuthProvider: phoneAuthProvider + ) + } + + var body: some View { + AuthPickerView { + SignInWithGoogleButton() + SignInWithFacebookButton() + PhoneAuthButtonView() + }.environment(authService) + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Info.plist b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Info.plist new file mode 100644 index 0000000000..968fe3cff8 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.406099696497-134k3722m01rtrsklhf3b7k8sqa5r7in + fb128693022464535 + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + app-1-406099696497-ios-58cbc26aca8e5cf83574d0 + + + + + FacebookAppID + 128693022464535 + FacebookClientToken + 16dbbdf0cfb309034a6ad98ac2a21688 + FacebookDisplayName + Firebase Swift UI App + LSApplicationQueriesSchemes + + fbapi + fb-messenger-share-api + + UIBackgroundModes + + fetch + remote-notification + + FirebaseAppDelegateProxyEnabled + + + diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Preview Content/Preview Assets.xcassets/Contents.json b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift new file mode 100644 index 0000000000..7d58ebed9c --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift @@ -0,0 +1,15 @@ +// +// FirebaseSwiftUIExampleTests.swift +// FirebaseSwiftUIExampleTests +// +// Created by Russell Wheatley on 18/02/2025. +// + +@testable import FirebaseSwiftUIExample +import Testing + +struct FirebaseSwiftUIExampleTests { + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift new file mode 100644 index 0000000000..299be7c8d5 --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift @@ -0,0 +1,45 @@ +// +// FirebaseSwiftUIExampleUITests.swift +// FirebaseSwiftUIExampleUITests +// +// Created by Russell Wheatley on 18/02/2025. +// + +import XCTest + +final class FirebaseSwiftUIExampleUITests: XCTestCase { + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the + // class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - + // required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITestsLaunchTests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITestsLaunchTests.swift new file mode 100644 index 0000000000..4ed3df1bfa --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// FirebaseSwiftUIExampleUITestsLaunchTests.swift +// FirebaseSwiftUIExampleUITests +// +// Created by Russell Wheatley on 18/02/2025. +// + +import XCTest + +final class FirebaseSwiftUIExampleUITestsLaunchTests: XCTestCase { + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/GoogleService-Info.plist b/samples/swiftui/FirebaseSwiftUIExample/GoogleService-Info.plist new file mode 100644 index 0000000000..f325ead98d --- /dev/null +++ b/samples/swiftui/FirebaseSwiftUIExample/GoogleService-Info.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 406099696497-134k3722m01rtrsklhf3b7k8sqa5r7in.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.406099696497-134k3722m01rtrsklhf3b7k8sqa5r7in + ANDROID_CLIENT_ID + 406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com + API_KEY + AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c + GCM_SENDER_ID + 406099696497 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.firebase.auth.example + PROJECT_ID + flutterfire-e2e-tests + STORAGE_BUCKET + flutterfire-e2e-tests.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:406099696497:ios:58cbc26aca8e5cf83574d0 + DATABASE_URL + https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app + + \ No newline at end of file