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
}
}
}