From d1f561c9921b430f97872a86995452ba27e8d754 Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 11:09:27 +0100
Subject: [PATCH 1/8] feat: initial implementation of apple architecture

---
 .../Services/AccountService+Apple.swift       | 49 +++++++++++++
 .../Services/AppleProviderAuthUI.swift        | 68 +++++++++++++++++++
 .../Sources/Services/AuthService+Apple.swift  | 33 +++++++++
 .../Sources/Views/SignInWithAppleButton.swift | 56 +++++++++++++++
 4 files changed, 206 insertions(+)
 create mode 100644 FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift
 create mode 100644 FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
 create mode 100644 FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift
 create mode 100644 FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift

diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift
new file mode 100644
index 00000000000..7005395a1f1
--- /dev/null
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift
@@ -0,0 +1,49 @@
+//
+//  AccountService+Apple.swift
+//  FirebaseUI
+//
+//  Created by Russell Wheatley on 21/10/2025.
+//
+
+@preconcurrency import FirebaseAuth
+import FirebaseAuthSwiftUI
+import Observation
+
+protocol AppleOperationReauthentication {
+  var appleProvider: AppleProviderSwift { get }
+}
+
+extension AppleOperationReauthentication {
+  @MainActor func reauthenticate() async throws -> AuthenticationToken {
+    guard let user = Auth.auth().currentUser else {
+      throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
+    }
+
+    do {
+      // TODO: Implement Apple reauthentication
+      let credential = try await appleProvider.createAuthCredential()
+      try await user.reauthenticate(with: credential)
+
+      return .firebase("")
+    } catch {
+      throw AuthServiceError.signInFailed(underlying: error)
+    }
+  }
+}
+
+@MainActor
+class AppleDeleteUserOperation: AuthenticatedOperation,
+  @preconcurrency AppleOperationReauthentication {
+  let appleProvider: AppleProviderSwift
+  init(appleProvider: AppleProviderSwift) {
+    self.appleProvider = appleProvider
+  }
+
+  func callAsFunction(on user: User) async throws {
+    // TODO: Implement delete user operation
+    try await callAsFunction(on: user) {
+      try await user.delete()
+    }
+  }
+}
+
diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
new file mode 100644
index 00000000000..b3ee81c7dc8
--- /dev/null
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
@@ -0,0 +1,68 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import FirebaseAuth
+import FirebaseAuthSwiftUI
+import FirebaseCore
+import SwiftUI
+
+public class AppleProviderSwift: AuthProviderSwift, DeleteUserSwift {
+  public let scopes: [String]
+  let providerId = "apple.com"
+
+  public init(scopes: [String] = []) {
+    self.scopes = scopes
+  }
+
+  @MainActor public func createAuthCredential() async throws -> AuthCredential {
+    // TODO: Implement Apple Sign In credential creation
+    // This will need to use ASAuthorizationAppleIDProvider
+    let provider = OAuthProvider(providerID: providerId)
+    return try await withCheckedThrowingContinuation { continuation in
+      provider.getCredentialWith(nil) { credential, error in
+        if let error {
+          continuation
+            .resume(throwing: AuthServiceError.signInFailed(underlying: error))
+        } else if let credential {
+          continuation.resume(returning: credential)
+        } else {
+          continuation
+            .resume(throwing: AuthServiceError
+              .invalidCredentials("Apple did not provide a valid AuthCredential"))
+        }
+      }
+    }
+  }
+
+  public func deleteUser(user: User) async throws {
+    // TODO: Implement delete user functionality
+    let operation = AppleDeleteUserOperation(appleProvider: self)
+    try await operation(on: user)
+  }
+}
+
+public class AppleProviderAuthUI: AuthProviderUI {
+  public var provider: AuthProviderSwift
+
+  public init(provider: AuthProviderSwift) {
+    self.provider = provider
+  }
+
+  public let id: String = "apple.com"
+
+  @MainActor public func authButton() -> AnyView {
+    AnyView(SignInWithAppleButton(provider: provider))
+  }
+}
+
diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift
new file mode 100644
index 00000000000..ae63f990463
--- /dev/null
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift
@@ -0,0 +1,33 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//
+//  AuthService+Apple.swift
+//  FirebaseUI
+//
+//  Created by Russell Wheatley on 21/10/2025.
+//
+
+import FirebaseAuthSwiftUI
+
+public extension AuthService {
+  @discardableResult
+  func withAppleSignIn(_ provider: AppleProviderSwift? = nil) -> AuthService {
+    // TODO: Register Apple provider with authentication service
+    registerProvider(providerWithButton: AppleProviderAuthUI(provider: provider ??
+        AppleProviderSwift()))
+    return self
+  }
+}
+
diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift
new file mode 100644
index 00000000000..47691bf016a
--- /dev/null
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift
@@ -0,0 +1,56 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import FirebaseAuthSwiftUI
+import SwiftUI
+
+/// A button for signing in with Apple
+@MainActor
+public struct SignInWithAppleButton {
+  @Environment(AuthService.self) private var authService
+  let provider: AuthProviderSwift
+  public init(provider: AuthProviderSwift) {
+    self.provider = provider
+  }
+}
+
+extension SignInWithAppleButton: View {
+  public var body: some View {
+    Button(action: {
+      // TODO: Implement sign in with Apple action
+      Task {
+        try await authService.signIn(provider)
+      }
+    }) {
+      HStack {
+        // TODO: Add Apple logo image
+        Image(systemName: "apple.logo")
+          .resizable()
+          .renderingMode(.template)
+          .scaledToFit()
+          .frame(width: 24, height: 24)
+          .foregroundColor(.white)
+        Text("Sign in with Apple")
+          .fontWeight(.semibold)
+          .foregroundColor(.white)
+      }
+      .frame(maxWidth: .infinity, alignment: .leading)
+      .padding()
+      .background(Color.black)
+      .cornerRadius(8)
+    }
+    .accessibilityIdentifier("sign-in-with-apple-button")
+  }
+}
+

From 35d55ba324184e446c92ee5dd49c8454f9919c39 Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 11:21:11 +0100
Subject: [PATCH 2/8] chore: setup package.swift with apple package

---
 .../FirebaseAppleSwiftUITests.swift           | 21 +++++++++++++++++++
 Package.swift                                 | 16 ++++++++++++++
 2 files changed, 37 insertions(+)
 create mode 100644 FirebaseSwiftUI/FirebaseAppleSwiftUI/Tests/FirebaseAppleSwiftUITests/FirebaseAppleSwiftUITests.swift

diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Tests/FirebaseAppleSwiftUITests/FirebaseAppleSwiftUITests.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Tests/FirebaseAppleSwiftUITests/FirebaseAppleSwiftUITests.swift
new file mode 100644
index 00000000000..03a8b65f073
--- /dev/null
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Tests/FirebaseAppleSwiftUITests/FirebaseAppleSwiftUITests.swift
@@ -0,0 +1,21 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@testable import FirebaseAppleSwiftUI
+import Testing
+
+@Test func example() async throws {
+  // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+}
+
diff --git a/Package.swift b/Package.swift
index d51a4f9ce4e..c26c7660083 100644
--- a/Package.swift
+++ b/Package.swift
@@ -82,6 +82,10 @@ let package = Package(
       name: "FirebaseTwitterSwiftUI",
       targets: ["FirebaseTwitterSwiftUI"]
     ),
+    .library(
+      name: "FirebaseAppleSwiftUI",
+      targets: ["FirebaseAppleSwiftUI"]
+    ),
   ],
   dependencies: [
     .package(
@@ -326,5 +330,17 @@ let package = Package(
       dependencies: ["FirebaseTwitterSwiftUI"],
       path: "FirebaseSwiftUI/FirebaseTwitterSwiftUI/Tests/"
     ),
+    .target(
+      name: "FirebaseAppleSwiftUI",
+      dependencies: [
+        "FirebaseAuthSwiftUI",
+      ],
+      path: "FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources"
+    ),
+    .testTarget(
+      name: "FirebaseAppleSwiftUITests",
+      dependencies: ["FirebaseAppleSwiftUI"],
+      path: "FirebaseSwiftUI/FirebaseAppleSwiftUI/Tests/"
+    ),
   ]
 )

From 5ccebec51a43f8f3f3303e36484dfe63ebcd4d41 Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 11:37:43 +0100
Subject: [PATCH 3/8] chore: import apple to test app

---
 .../FirebaseSwiftUIExample.xcodeproj/project.pbxproj      | 8 ++++++++
 .../FirebaseSwiftUIExample/ContentView.swift              | 4 +++-
 .../FirebaseSwiftUIExample/TestView.swift                 | 3 +++
 3 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj
index 12a89316287..d593bafac1a 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj
@@ -11,6 +11,7 @@
 		4600E5542DD777BE00EED5F3 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4600E5532DD777BE00EED5F3 /* FirebaseCore */; };
 		4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */; };
 		4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */; };
+		4610DD2A2EA796360084B32B /* FirebaseAppleSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4610DD292EA796360084B32B /* FirebaseAppleSwiftUI */; };
 		4681E0002E97F22B00387C88 /* FirebaseTwitterSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */; };
 		46CB7B252D773F2100F1FD0A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 46CB7B242D773F2100F1FD0A /* GoogleService-Info.plist */; };
 		46F89C392D64B04E000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 46F89C382D64B04E000F8BC0 /* FirebaseAuthSwiftUI */; };
@@ -81,6 +82,7 @@
 			files = (
 				8D808CB72DB0811900D2293F /* FirebaseFacebookSwiftUI in Frameworks */,
 				46F89C392D64B04E000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */,
+				4610DD2A2EA796360084B32B /* FirebaseAppleSwiftUI in Frameworks */,
 				46F89C4D2D64BB9B000F8BC0 /* FirebaseAuthSwiftUI in Frameworks */,
 				4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */,
 				8D808CB92DB081F900D2293F /* FirebasePhoneAuthSwiftUI in Frameworks */,
@@ -164,6 +166,7 @@
 				8D808CB62DB0811900D2293F /* FirebaseFacebookSwiftUI */,
 				8D808CB82DB081F900D2293F /* FirebasePhoneAuthSwiftUI */,
 				4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */,
+				4610DD292EA796360084B32B /* FirebaseAppleSwiftUI */,
 			);
 			productName = FirebaseSwiftUIExample;
 			productReference = 46F89C082D64A86C000F8BC0 /* FirebaseSwiftUIExample.app */;
@@ -663,6 +666,11 @@
 			isa = XCSwiftPackageProductDependency;
 			productName = FirebaseGoogleSwiftUI;
 		};
+		4610DD292EA796360084B32B /* FirebaseAppleSwiftUI */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 8D808CB52DB07EBD00D2293F /* XCLocalSwiftPackageReference "../../../../FirebaseUI-iOS" */;
+			productName = FirebaseAppleSwiftUI;
+		};
 		4681DFFF2E97F22B00387C88 /* FirebaseTwitterSwiftUI */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 8D808CB52DB07EBD00D2293F /* XCLocalSwiftPackageReference "../../../../FirebaseUI-iOS" */;
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift
index 4c29bf00733..1101a55e364 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift
@@ -24,7 +24,8 @@ import FirebaseAuthSwiftUI
 import FirebaseFacebookSwiftUI
 import FirebaseGoogleSwiftUI
 import FirebasePhoneAuthSwiftUI
-import FirebaseTwitterSwiftUI 
+import FirebaseTwitterSwiftUI
+import FirebaseAppleSwiftUI
 import SwiftUI
 
 struct ContentView: View {
@@ -49,6 +50,7 @@ struct ContentView: View {
     )
     .withGoogleSignIn()
     .withPhoneSignIn()
+    .withAppleSignIn()
     .withTwitterSignIn()
     .withFacebookSignIn()
     .withEmailSignIn()
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift
index 5c074a23980..04aef854829 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift
@@ -24,6 +24,8 @@ import FirebaseAuthSwiftUI
 import FirebaseFacebookSwiftUI
 import FirebaseGoogleSwiftUI
 import FirebasePhoneAuthSwiftUI
+import FirebaseAppleSwiftUI
+import FirebaseTwitterSwiftUI
 import SwiftUI
 
 struct TestView: View {
@@ -56,6 +58,7 @@ struct TestView: View {
     )
     .withGoogleSignIn()
     .withPhoneSignIn()
+    .withAppleSignIn()
     .withTwitterSignIn()
     .withFacebookSignIn()
     .withEmailSignIn()

From 7f6be5506b8b4fb35a5a223b17ca1cf6efb81518 Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 12:07:40 +0100
Subject: [PATCH 4/8] feat: support apple sign in

---
 .../Services/AccountService+Apple.swift       |   2 -
 .../Services/AppleProviderAuthUI.swift        | 121 +++++++++++++++---
 .../Sources/Services/AuthService+Apple.swift  |   1 -
 .../Sources/Services/CryptoUtils.swift        |  53 ++++++++
 .../Sources/Views/SignInWithAppleButton.swift |   2 -
 5 files changed, 157 insertions(+), 22 deletions(-)
 create mode 100644 FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/CryptoUtils.swift

diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift
index 7005395a1f1..101eee77457 100644
--- a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift
@@ -20,7 +20,6 @@ extension AppleOperationReauthentication {
     }
 
     do {
-      // TODO: Implement Apple reauthentication
       let credential = try await appleProvider.createAuthCredential()
       try await user.reauthenticate(with: credential)
 
@@ -40,7 +39,6 @@ class AppleDeleteUserOperation: AuthenticatedOperation,
   }
 
   func callAsFunction(on user: User) async throws {
-    // TODO: Implement delete user operation
     try await callAsFunction(on: user) {
       try await user.delete()
     }
diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
index b3ee81c7dc8..1524526a168 100644
--- a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
@@ -12,11 +12,103 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import AuthenticationServices
+import CryptoKit
 import FirebaseAuth
 import FirebaseAuthSwiftUI
 import FirebaseCore
 import SwiftUI
 
+// MARK: - Data Extensions
+
+extension Data {
+  var utf8String: String? {
+    return String(data: self, encoding: .utf8)
+  }
+}
+
+extension ASAuthorizationAppleIDCredential {
+  var authorizationCodeString: String? {
+    return authorizationCode?.utf8String
+  }
+
+  var idTokenString: String? {
+    return identityToken?.utf8String
+  }
+}
+
+// MARK: - Authenticate With Apple Dialog
+
+private func authenticateWithApple() async throws -> (ASAuthorizationAppleIDCredential, String) {
+  return try await AuthenticateWithAppleDialog().authenticate()
+}
+
+private class AuthenticateWithAppleDialog: NSObject {
+  private var continuation: CheckedContinuation<(ASAuthorizationAppleIDCredential, String), Error>?
+  private var currentNonce: String?
+
+  func authenticate() async throws -> (ASAuthorizationAppleIDCredential, String) {
+    return try await withCheckedThrowingContinuation { continuation in
+      self.continuation = continuation
+
+      let appleIDProvider = ASAuthorizationAppleIDProvider()
+      let request = appleIDProvider.createRequest()
+      request.requestedScopes = [.fullName, .email]
+
+      do {
+        let nonce = try CryptoUtils.randomNonceString()
+        currentNonce = nonce
+        request.nonce = CryptoUtils.sha256(nonce)
+      } catch {
+        continuation.resume(throwing: AuthServiceError.signInFailed(underlying: error))
+        return
+      }
+
+      let authorizationController = ASAuthorizationController(authorizationRequests: [request])
+      authorizationController.delegate = self
+      authorizationController.performRequests()
+    }
+  }
+}
+
+extension AuthenticateWithAppleDialog: ASAuthorizationControllerDelegate {
+  func authorizationController(
+    controller: ASAuthorizationController,
+    didCompleteWithAuthorization authorization: ASAuthorization
+  ) {
+    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
+      if let nonce = currentNonce {
+        continuation?.resume(returning: (appleIDCredential, nonce))
+      } else {
+        continuation?.resume(
+          throwing: AuthServiceError.signInFailed(
+            underlying: NSError(
+              domain: "AppleSignIn",
+              code: -1,
+              userInfo: [NSLocalizedDescriptionKey: "Missing nonce"]
+            )
+          )
+        )
+      }
+    } else {
+      continuation?.resume(
+        throwing: AuthServiceError.invalidCredentials("Missing Apple ID credential")
+      )
+    }
+    continuation = nil
+  }
+
+  func authorizationController(
+    controller: ASAuthorizationController,
+    didCompleteWithError error: Error
+  ) {
+    continuation?.resume(throwing: AuthServiceError.signInFailed(underlying: error))
+    continuation = nil
+  }
+}
+
+// MARK: - Apple Provider Swift
+
 public class AppleProviderSwift: AuthProviderSwift, DeleteUserSwift {
   public let scopes: [String]
   let providerId = "apple.com"
@@ -26,27 +118,22 @@ public class AppleProviderSwift: AuthProviderSwift, DeleteUserSwift {
   }
 
   @MainActor public func createAuthCredential() async throws -> AuthCredential {
-    // TODO: Implement Apple Sign In credential creation
-    // This will need to use ASAuthorizationAppleIDProvider
-    let provider = OAuthProvider(providerID: providerId)
-    return try await withCheckedThrowingContinuation { continuation in
-      provider.getCredentialWith(nil) { credential, error in
-        if let error {
-          continuation
-            .resume(throwing: AuthServiceError.signInFailed(underlying: error))
-        } else if let credential {
-          continuation.resume(returning: credential)
-        } else {
-          continuation
-            .resume(throwing: AuthServiceError
-              .invalidCredentials("Apple did not provide a valid AuthCredential"))
-        }
-      }
+    let (appleIDCredential, nonce) = try await authenticateWithApple()
+
+    guard let idTokenString = appleIDCredential.idTokenString else {
+      throw AuthServiceError.invalidCredentials("Unable to fetch identity token from Apple")
     }
+
+    let credential = OAuthProvider.appleCredential(
+      withIDToken: idTokenString,
+      rawNonce: nonce,
+      fullName: appleIDCredential.fullName
+    )
+
+    return credential
   }
 
   public func deleteUser(user: User) async throws {
-    // TODO: Implement delete user functionality
     let operation = AppleDeleteUserOperation(appleProvider: self)
     try await operation(on: user)
   }
diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift
index ae63f990463..57e6e11576e 100644
--- a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift
@@ -24,7 +24,6 @@ import FirebaseAuthSwiftUI
 public extension AuthService {
   @discardableResult
   func withAppleSignIn(_ provider: AppleProviderSwift? = nil) -> AuthService {
-    // TODO: Register Apple provider with authentication service
     registerProvider(providerWithButton: AppleProviderAuthUI(provider: provider ??
         AppleProviderSwift()))
     return self
diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/CryptoUtils.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/CryptoUtils.swift
new file mode 100644
index 00000000000..d09fc9bf653
--- /dev/null
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/CryptoUtils.swift
@@ -0,0 +1,53 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+import CryptoKit
+
+/// Set of utility APIs for generating cryptographical artifacts.
+enum CryptoUtils {
+  enum NonceGenerationError: Error {
+    case generationFailure(status: OSStatus)
+  }
+
+  static func randomNonceString(length: Int = 32) throws -> String {
+    precondition(length > 0)
+    var randomBytes = [UInt8](repeating: 0, count: length)
+    let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
+    if errorCode != errSecSuccess {
+      throw NonceGenerationError.generationFailure(status: errorCode)
+    }
+
+    let charset: [Character] =
+      Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
+
+    let nonce = randomBytes.map { byte in
+      // Pick a random character from the set, wrapping around if needed.
+      charset[Int(byte) % charset.count]
+    }
+
+    return String(nonce)
+  }
+
+  static func sha256(_ input: String) -> String {
+    let inputData = Data(input.utf8)
+    let hashedData = SHA256.hash(data: inputData)
+    let hashString = hashedData.compactMap {
+      String(format: "%02x", $0)
+    }.joined()
+
+    return hashString
+  }
+}
+
diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift
index 47691bf016a..df6f4b40f53 100644
--- a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift
@@ -28,13 +28,11 @@ public struct SignInWithAppleButton {
 extension SignInWithAppleButton: View {
   public var body: some View {
     Button(action: {
-      // TODO: Implement sign in with Apple action
       Task {
         try await authService.signIn(provider)
       }
     }) {
       HStack {
-        // TODO: Add Apple logo image
         Image(systemName: "apple.logo")
           .resizable()
           .renderingMode(.template)

From 0cbc68e1e95f87be3ffa93bbfd47898438d9332b Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 12:37:19 +0100
Subject: [PATCH 5/8] chore: example app has apple sign in capability

---
 .../FirebaseSwiftUIExample.entitlements                       | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements
index ea83d33fa90..f817dae27e6 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExample.entitlements
@@ -4,6 +4,10 @@
 <dict>
 	<key>aps-environment</key>
 	<string>development</string>
+	<key>com.apple.developer.applesignin</key>
+	<array>
+		<string>Default</string>
+	</array>
 	<key>com.apple.developer.associated-domains</key>
 	<array>
 		<string>applinks:flutterfire-e2e-tests.firebaseapp.com</string>

From d595a8b3bacb67113d96f6963d0506fc5e597d29 Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 13:08:39 +0100
Subject: [PATCH 6/8] fix: store pending credential to allow deletion of user

---
 .../Sources/Services/AuthService.swift                   | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift
index 2ba0ed5a6e6..2f6bfcf472a 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift
@@ -107,6 +107,7 @@ public final class AuthService {
   public let passwordPrompt: PasswordPromptCoordinator = .init()
   public var currentMFARequired: MFARequired?
   private var currentMFAResolver: MultiFactorResolver?
+  private var pendingMFACredential: AuthCredential?
 
   // MARK: - Provider APIs
 
@@ -234,6 +235,8 @@ public final class AuthService {
       if error.code == AuthErrorCode.secondFactorRequired.rawValue {
         if let resolver = error
           .userInfo[AuthErrorUserInfoMultiFactorResolverKey] as? MultiFactorResolver {
+          // Preserve the original credential for use after MFA resolution
+          pendingMFACredential = credentials
           return handleMFARequiredError(resolver: resolver)
         }
       }
@@ -847,12 +850,16 @@ public extension AuthService {
 
     do {
       let result = try await resolver.resolveSignIn(with: assertion)
-      signedInCredential = result.credential
+      
+      // After MFA resolution, result.credential is nil, so restore the original credential
+      // that was used before MFA was triggered
+      signedInCredential = result.credential ?? pendingMFACredential
       updateAuthenticationState()
 
       // Clear MFA resolution state
       currentMFARequired = nil
       currentMFAResolver = nil
+      pendingMFACredential = nil
 
     } catch {
       throw AuthServiceError

From e92dc97ee07abb97310ed1129ffe508fef696779 Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 13:35:36 +0100
Subject: [PATCH 7/8] test: test the existence of apple button

---
 .../FirebaseSwiftUIExampleUITests.swift                    | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift
index 524d3511e44..7dbc0dc2f41 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift
@@ -54,6 +54,13 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase {
       twitterButton.waitForExistence(timeout: 5),
       "Twitter/X sign-in button should exist"
     )
+
+    // Check for Apple sign-in button
+    let appleButton = app.buttons["sign-in-with-apple-button"]
+    XCTAssertTrue(
+      appleButton.waitForExistence(timeout: 5),
+      "Apple sign-in button should exist"
+    )
     
     // Check for Google sign-in button
     let googleButton = app.buttons["sign-in-with-google-button"]

From ff554e4c506870a833550ccd4aa5c469920e9b6d Mon Sep 17 00:00:00 2001
From: russellwheatley <russellwheatley85@gmail.com>
Date: Tue, 21 Oct 2025 13:45:01 +0100
Subject: [PATCH 8/8] fix: pass in apple scopes

---
 .../Services/AppleProviderAuthUI.swift        | 20 +++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
index 1524526a168..6ca6ea04cb7 100644
--- a/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
+++ b/FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift
@@ -39,13 +39,21 @@ extension ASAuthorizationAppleIDCredential {
 
 // MARK: - Authenticate With Apple Dialog
 
-private func authenticateWithApple() async throws -> (ASAuthorizationAppleIDCredential, String) {
-  return try await AuthenticateWithAppleDialog().authenticate()
+private func authenticateWithApple(
+  scopes: [ASAuthorization.Scope]
+) async throws -> (ASAuthorizationAppleIDCredential, String) {
+  return try await AuthenticateWithAppleDialog(scopes: scopes).authenticate()
 }
 
 private class AuthenticateWithAppleDialog: NSObject {
   private var continuation: CheckedContinuation<(ASAuthorizationAppleIDCredential, String), Error>?
   private var currentNonce: String?
+  private let scopes: [ASAuthorization.Scope]
+  
+  init(scopes: [ASAuthorization.Scope]) {
+    self.scopes = scopes
+    super.init()
+  }
 
   func authenticate() async throws -> (ASAuthorizationAppleIDCredential, String) {
     return try await withCheckedThrowingContinuation { continuation in
@@ -53,7 +61,7 @@ private class AuthenticateWithAppleDialog: NSObject {
 
       let appleIDProvider = ASAuthorizationAppleIDProvider()
       let request = appleIDProvider.createRequest()
-      request.requestedScopes = [.fullName, .email]
+      request.requestedScopes = scopes
 
       do {
         let nonce = try CryptoUtils.randomNonceString()
@@ -110,15 +118,15 @@ extension AuthenticateWithAppleDialog: ASAuthorizationControllerDelegate {
 // MARK: - Apple Provider Swift
 
 public class AppleProviderSwift: AuthProviderSwift, DeleteUserSwift {
-  public let scopes: [String]
+  public let scopes: [ASAuthorization.Scope]
   let providerId = "apple.com"
 
-  public init(scopes: [String] = []) {
+  public init(scopes: [ASAuthorization.Scope] = [.fullName, .email]) {
     self.scopes = scopes
   }
 
   @MainActor public func createAuthCredential() async throws -> AuthCredential {
-    let (appleIDCredential, nonce) = try await authenticateWithApple()
+    let (appleIDCredential, nonce) = try await authenticateWithApple(scopes: scopes)
 
     guard let idTokenString = appleIDCredential.idTokenString else {
       throw AuthServiceError.invalidCredentials("Unable to fetch identity token from Apple")
