Issue #808

Make SignInWithAppleButton

Wrap ASAuthorizationAppleIDButton inside UIViewRepresentable

import SwiftUI
import UIKit
import AuthenticationServices

struct SignInWithAppleButton: View {
    @Environment(\.colorScheme)
    private var colorScheme: ColorScheme
    
    var body: some View {
        ButtonInside(colorScheme: colorScheme)
            .frame(width: 280, height: 45)
    }
 }

private struct ButtonInside: UIViewRepresentable {
    let colorScheme: ColorScheme
    
    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        switch colorScheme {
        case .dark:
            return ASAuthorizationAppleIDButton(type: .signIn, style: .white)
        default:
            return ASAuthorizationAppleIDButton(type: .signIn, style: .black)
        }
    }
    
    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
        // No op
    }
}

struct SignInWithAppleButton_Previews: PreviewProvider {
    static var previews: some View {
        SignInWithAppleButton()
    }
}

Handle logic in ViewModel

import SwiftUI
import AuthenticationServices
import Resolver
import Firebase
import FirebaseAuth

final class AuthViewModel: NSObject, ObservableObject {
    enum RequestState: String {
        case signIn
        case link
        case reauth
    }

    let firebaseService: FirebaseService
    var window: UIWindow?
    private var nonceGenerator = NonceGenerator()
    let requestState: RequestState = .signIn
    
    func signInWithApple() {
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]
        request.nonce = nonceGenerator.nonce
        request.state = RequestState.link.rawValue
        
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        if let window = window {
            let provider = ContextProvider(window: window)
            controller.presentationContextProvider = provider
        }
        controller.performRequests()
    }
}

extension AuthViewModel: ASAuthorizationControllerDelegate {
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        guard
            let appleIdCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
            let tokenData = appleIdCredential.identityToken,
            let token = String(data: tokenData, encoding: .utf8),
            let stateRawValue = appleIdCredential.state,
            let requestState = RequestState(rawValue: stateRawValue),
            requestState == .link
        else {
            return
        }
        
        let credential = OAuthProvider.credential(
            withProviderID: "apple.com",
            idToken: token,
            rawNonce: nonceGenerator.nonce
        )
        
        firebaseService.signIn(credential: credential)
    }
}

private final class ContextProvider: NSObject, ASAuthorizationControllerPresentationContextProviding {
    private let window: UIWindow
    
    init(window: UIWindow) {
        self.window = window
    }
    
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        window
    }
}

Make a nonce generator to keep current nonce

import Foundation
import CryptoKit

struct NonceGenerator {
    var nonce: String = ""
    
    init() {
        generate()
    }
    
    mutating func generate() {
        nonce = sha256(input: randomNonceString())
    }
    
    private func sha256(input: String) -> String {
        let inputData = Data(input.utf8)
        let hashedData = SHA256.hash(data: inputData)
        let hashString = hashedData.compactMap {
          return String(format: "%02x", $0)
        }.joined()

        return hashString
    }
    
    // https://firebase.google.com/docs/auth/ios/apple
    private func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        let charset: Array<Character> =
            Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
        var result = ""
        var remainingLength = length
        
        while remainingLength > 0 {
            let randoms: [UInt8] = (0 ..< 16).map { _ in
                var random: UInt8 = 0
                let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
                if errorCode != errSecSuccess {
                    fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
                }
                return random
            }
            
            randoms.forEach { random in
                if remainingLength == 0 {
                    return
                }
                
                if random < charset.count {
                    result.append(charset[Int(random)])
                    remainingLength -= 1
                }
            }
        }
        
        return result
    }
}

In FirebaseService

Auth.auth().signIn(with: credential)

Handle in AuthView

Make window PreferenceKey to pass into our AuthView

import SwiftUI
import UIKit

struct WindowKey: EnvironmentKey {
    struct Value {
        weak var value: UIWindow?
    }
    
    static let defaultValue: Value = .init(value: nil)
}

extension EnvironmentValues {
    var window: UIWindow? {
        get { return self[WindowKey.self].value }
        set { self[WindowKey.self] = .init(value: newValue) }
    }
}

Assign the window to our ViewModel. Ideally this setup should be done in AuthCoordinator which inject AuthViewModel with correct UIWindow

import SwiftUI

struct AuthView: View {
    @Environment(\.window) var window: UIWindow?
    @StateObject var viewModel = AuthViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                SignInWithAppleButton()
                    .onTapGesture {
                        viewModel.signInWithApple()
                    }
            }
        }
        .onAppear {
            viewModel.window = window
        }
    }

}

Read more