Skip to content

Commit

Permalink
Merge pull request #9 from justbcuz/anonymous-sign-in
Browse files Browse the repository at this point in the history
Anonymous Sign In
  • Loading branch information
SwiftfulThinking authored May 12, 2024
2 parents 3580eeb + c153b3f commit 8073a7b
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 6 deletions.
11 changes: 11 additions & 0 deletions Sources/SwiftfulFirebaseAuth/Core/AuthManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ public final class AuthManager {
return value
}

public func signInAnonymously() async throws -> (user: UserAuthInfo, isNewUser: Bool) {
let value = try await provider.authenticateUser_Anonymously()
currentUser = AuthInfo(profile: value.user)

defer {
streamSignInChangesIfNeeded()
}

return value
}

public func signInPhone_Start(phoneNumber: String) async throws {
try await provider.authenticateUser_PhoneNumber_Start(phoneNumber: phoneNumber)
}
Expand Down
97 changes: 97 additions & 0 deletions Sources/SwiftfulFirebaseAuth/Core/SignInAnonymousButtonView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// SignInAnonymousButtonView.swift
//
//
// Created by Nick Sarno on 5/11/24.
//

import SwiftUI
import SwiftUI
import AuthenticationServices

// TODO: Streamline all Sign In Buttons
// Note: Most Anonymous sign-in should happen in the background and not be a button the user clicks.

public struct SignInAnonymousButtonView: View {

private var backgroundColor: Color
private var foregroundColor: Color
private var borderColor: Color
private var buttonText: String
private var cornerRadius: CGFloat

public init(
type: ASAuthorizationAppleIDButton.ButtonType = .signIn,
style: ASAuthorizationAppleIDButton.Style = .black,
cornerRadius: CGFloat = 10
) {
self.cornerRadius = cornerRadius
self.backgroundColor = style.backgroundColor
self.foregroundColor = style.foregroundColor
self.borderColor = style.borderColor
self.buttonText = type.buttonText.removingWord(" with")
}

public var body: some View {
ZStack {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(borderColor)

RoundedRectangle(cornerRadius: cornerRadius)
.fill(backgroundColor)
.padding(0.8)

HStack(spacing: 8) {
Image(systemName: "person.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)

Text("\(buttonText) Anonymously")
.font(.system(size: 21))
.fontWeight(.medium)
}
.foregroundColor(foregroundColor)
}
.padding(.vertical, 1)
.disabled(true)
}
}

fileprivate extension String {
func removingWord(_ word: String) -> String {
let pattern = "\\b\(word)\\b"
return self.replacingOccurrences(of: pattern, with: "", options: .regularExpression)
}
}

#Preview("SignInAnonymousButtonView") {
VStack {
SignInWithAppleButtonView()
.frame(height: 60)
.padding()
SignInWithGoogleButtonView()
.frame(height: 60)
.padding()
SignInWithPhoneButtonView()
.frame(height: 60)
.padding()

if #available(iOS 14.0, *) {
SignInAnonymousButtonView(type: .signUp)
.frame(height: 60)
.padding()
}

SignInAnonymousButtonView(type: .continue)
.frame(height: 60)
.padding()

SignInAnonymousButtonView(type: .signIn)
.frame(height: 60)
.padding()
SignInAnonymousButtonView(type: .default)
.frame(height: 60)
.padding()
}
}
15 changes: 14 additions & 1 deletion Sources/SwiftfulFirebaseAuth/Providers/AuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import FirebaseAuth
public protocol AuthProvider {
func getAuthenticatedUser() -> UserAuthInfo?
func authenticationDidChangeStream() -> AsyncStream<UserAuthInfo?>
func authenticateUser_Anonymously() async throws -> (user: UserAuthInfo, isNewUser: Bool)
func authenticateUser_Google(GIDClientID: String) async throws -> (user: UserAuthInfo, isNewUser: Bool)
func authenticateUser_Apple() async throws -> (user: UserAuthInfo, isNewUser: Bool)
func authenticateUser_PhoneNumber_Start(phoneNumber: String) async throws
Expand Down Expand Up @@ -62,7 +63,7 @@ public struct UserAuthInfo: Codable {
self.uid = user.uid
self.email = user.email
self.isAnonymous = user.isAnonymous
self.authProviders = user.providerData.compactMap({ AuthProviderOption(rawValue: $0.providerID) })
self.authProviders = UserAuthInfo.createAuthProviders(rawValues: user.providerData, isAnonymous: user.isAnonymous)
self.displayName = user.displayName
self.firstName = UserDefaults.auth.firstName
self.lastName = UserDefaults.auth.lastName
Expand All @@ -72,6 +73,17 @@ public struct UserAuthInfo: Codable {
self.lastSignInDate = user.metadata.lastSignInDate
}

static private func createAuthProviders(rawValues: [any UserInfo], isAnonymous: Bool) -> [AuthProviderOption] {
// Note: Firebase Anonymous auth does not return a string for Auth Provider. Here we catch that edge case.
var providers = rawValues.compactMap({ AuthProviderOption(rawValue: $0.providerID) })

if providers.isEmpty && isAnonymous {
providers.append(.anonymous)
}

return providers
}

enum CodingKeys: String, CodingKey {
case uid = "user_id"
case email = "email"
Expand All @@ -88,6 +100,7 @@ public struct UserAuthInfo: Codable {
}

public enum AuthProviderOption: String, Codable {
case anonymous = "anonymous"
case google = "google.com"
case apple = "apple.com"
case email = "password"
Expand Down
30 changes: 25 additions & 5 deletions Sources/SwiftfulFirebaseAuth/Providers/FirebaseAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ struct FirebaseAuthProvider: AuthProvider {
}
}

@MainActor
func authenticateUser_Anonymously() async throws -> (user: UserAuthInfo, isNewUser: Bool) {

// Sign in to Firebase
let authDataResult = try await Auth.auth().signInAnonymously()

// Determines if this is the first time this user is being authenticated
let isNewUser = authDataResult.additionalUserInfo?.isNewUser ?? true

// Convert to generic type
let user = UserAuthInfo(user: authDataResult.user)

return (user, isNewUser)
}

@MainActor
func authenticateUser_Apple() async throws -> (user: UserAuthInfo, isNewUser: Bool) {
let helper = SignInWithAppleHelper()
Expand All @@ -47,7 +62,7 @@ struct FirebaseAuthProvider: AuthProvider {
)

// Sign in to Firebase
let authDataResult = try await signIn(credential: credential)
let authDataResult = try await signInOrLink(credential: credential)

var firebaserUser = authDataResult.user

Expand Down Expand Up @@ -87,7 +102,7 @@ struct FirebaseAuthProvider: AuthProvider {
let credential = GoogleAuthProvider.credential(withIDToken: googleResponse.idToken, accessToken: googleResponse.accessToken)

// Sign in to Firebase
let authDataResult = try await signIn(credential: credential)
let authDataResult = try await signInOrLink(credential: credential)

var firebaserUser = authDataResult.user

Expand Down Expand Up @@ -132,7 +147,7 @@ struct FirebaseAuthProvider: AuthProvider {
let credential = PhoneAuthProvider.provider().credential(withVerificationID: verificationId, verificationCode: code)

// Sign in to Firebase
let authDataResult = try await signIn(credential: credential)
let authDataResult = try await signInOrLink(credential: credential)

let firebaserUser = authDataResult.user

Expand Down Expand Up @@ -160,8 +175,13 @@ struct FirebaseAuthProvider: AuthProvider {
// MARK: PRIVATE


private func signIn(credential: AuthCredential) async throws -> AuthDataResult {
try await Auth.auth().signIn(with: credential)
private func signInOrLink(credential: AuthCredential) async throws -> AuthDataResult {
// If user is anonymous, attempt to link credential to existing account. On failure, fall-back to signIn to create a new account.
if let user = Auth.auth().currentUser, user.isAnonymous, let result = try? await user.link(with: credential) {
return result
}

return try await Auth.auth().signIn(with: credential)
}

private func updateUserProfile(displayName: String?, firstName: String?, lastName: String?, photoUrl: URL?) async throws -> User? {
Expand Down
5 changes: 5 additions & 0 deletions Sources/SwiftfulFirebaseAuth/Providers/MockAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ final class MockAuthProvider: AuthProvider {
}
}

func authenticateUser_Anonymously() async throws -> (user: UserAuthInfo, isNewUser: Bool) {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return signInMockUser()
}

func authenticateUser_Google(GIDClientID: String) async throws -> (user: UserAuthInfo, isNewUser: Bool) {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return signInMockUser()
Expand Down

0 comments on commit 8073a7b

Please sign in to comment.