@@ -77,7 +77,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
77
77
// If the item's affiliated provider is currently linked with the user,
78
78
// unlink the provider from the user's account.
79
79
if item. isChecked {
80
- unlinkFromProvider ( provider. id)
80
+ Task { await unlinkFromProvider ( provider. id) }
81
81
return
82
82
}
83
83
@@ -86,7 +86,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
86
86
performGoogleAccountLink ( )
87
87
88
88
case . apple:
89
- performAppleAccountLink ( )
89
+ Task { await performAppleAccountLink ( ) }
90
90
91
91
case . facebook:
92
92
performFacebookAccountLink ( )
@@ -124,15 +124,53 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
124
124
}
125
125
}
126
126
127
+ /// Used for Sign in with Apple token revocation flow.
128
+ private var continuation : CheckedContinuation < ASAuthorizationAppleIDCredential , Error > ?
129
+
127
130
/// Wrapper method that uses Firebase's `unlink(fromProvider:)` API to unlink a user from an auth
128
131
/// provider.
129
132
/// This method will update the UI upon the unlinking's completion.
130
133
/// - Parameter providerID: The string id of the auth provider.
131
- private func unlinkFromProvider( _ providerID: String ) {
132
- user. unlink ( fromProvider: providerID) { user, error in
133
- guard error == nil else { return self . displayError ( error) }
134
- print ( " Unlinked user from auth provider: \( providerID) " )
135
- self . updateUI ( )
134
+ private func unlinkFromProvider( _ providerID: String ) async {
135
+ if providerID == AuthProviderID . apple. rawValue {
136
+ // Needs SiwA token revocation.
137
+ do {
138
+ let needsTokenRevocation = user. providerData
139
+ . contains { $0. providerID == AuthProviderID . apple. rawValue }
140
+ if needsTokenRevocation {
141
+ let appleIDCredential = try await signInWithApple ( )
142
+
143
+ guard let appleIDToken = appleIDCredential. identityToken else {
144
+ print ( " Unable to fetch identify token. " )
145
+ return
146
+ }
147
+ guard let idTokenString = String ( data: appleIDToken, encoding: . utf8) else {
148
+ print ( " Unable to serialise token string from data: \( appleIDToken. debugDescription) " )
149
+ return
150
+ }
151
+
152
+ let nonce = try CryptoUtils . randomNonceString ( )
153
+ let credential = OAuthProvider . credential ( providerID: . apple,
154
+ idToken: idTokenString,
155
+ rawNonce: nonce)
156
+
157
+ try await user. reauthenticate ( with: credential)
158
+ if
159
+ let authorizationCode = appleIDCredential. authorizationCode,
160
+ let authCodeString = String ( data: authorizationCode, encoding: . utf8) {
161
+ try await Auth . auth ( ) . revokeToken ( withAuthorizationCode: authCodeString)
162
+ }
163
+ }
164
+ } catch {
165
+ displayError ( error)
166
+ }
167
+ }
168
+
169
+ do {
170
+ _ = try await user. unlink ( fromProvider: providerID)
171
+ updateUI ( )
172
+ } catch {
173
+ displayError ( error)
136
174
}
137
175
}
138
176
@@ -179,27 +217,26 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
179
217
180
218
// MARK: - Sign in with Apple Account Linking 🔥
181
219
182
- // For Sign in with Apple
183
- var currentNonce : String ?
184
-
185
220
/// This method will initate the Sign In with Apple flow.
186
221
/// See this class's conformance to `ASAuthorizationControllerDelegate` below for
187
222
/// context on how the linking is made.
188
- private func performAppleAccountLink( ) {
223
+ private func performAppleAccountLink( ) async {
189
224
do {
190
- let nonce = try CryptoUtils . randomNonceString ( )
191
- currentNonce = nonce
192
- let appleIDProvider = ASAuthorizationAppleIDProvider ( )
193
- let request = appleIDProvider. createRequest ( )
194
- request. requestedScopes = [ . fullName, . email]
195
- request. nonce = CryptoUtils . sha256 ( nonce)
225
+ let appleIDCredential = try await signInWithApple ( )
196
226
197
- let authorizationController = ASAuthorizationController ( authorizationRequests: [ request] )
198
- authorizationController. delegate = self
199
- authorizationController. presentationContextProvider = self
200
- authorizationController. performRequests ( )
227
+ guard let appleIDToken = appleIDCredential. identityToken else {
228
+ fatalError ( " Unable to fetch identify token. " )
229
+ }
230
+ guard let idTokenString = String ( data: appleIDToken, encoding: . utf8) else {
231
+ fatalError ( " Unable to serialise token string from data: \( appleIDToken. debugDescription) " )
232
+ }
233
+
234
+ let nonce = try CryptoUtils . randomNonceString ( )
235
+ let credential = OAuthProvider . credential ( providerID: . apple,
236
+ idToken: idTokenString,
237
+ rawNonce: nonce)
238
+ linkAccount ( authCredential: credential)
201
239
} catch {
202
- // In the unlikely case that nonce generation fails, show error view.
203
240
displayError ( error)
204
241
}
205
242
}
@@ -448,7 +485,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
448
485
dataSourceProvider. delegate = self
449
486
}
450
487
451
- private func updateUI( ) {
488
+ @ MainActor private func updateUI( ) {
452
489
configureDataSourceProvider ( )
453
490
animateUpdates ( for: tableView)
454
491
}
@@ -488,39 +525,34 @@ extension AccountLinkingViewController: ASAuthorizationControllerDelegate,
488
525
ASAuthorizationControllerPresentationContextProviding {
489
526
// MARK: ASAuthorizationControllerDelegate
490
527
491
- func authorizationController( controller: ASAuthorizationController ,
492
- didCompleteWithAuthorization authorization: ASAuthorization ) {
493
- guard let appleIDCredential = authorization. credential as? ASAuthorizationAppleIDCredential
494
- else {
495
- print ( " Unable to retrieve AppleIDCredential " )
496
- return
497
- }
528
+ func signInWithApple( ) async throws -> ASAuthorizationAppleIDCredential {
529
+ return try await withCheckedThrowingContinuation { continuation in
530
+ self . continuation = continuation
531
+ let appleIDProvider = ASAuthorizationAppleIDProvider ( )
532
+ let request = appleIDProvider. createRequest ( )
533
+ request. requestedScopes = [ . fullName, . email]
498
534
499
- guard let nonce = currentNonce else {
500
- fatalError ( " Invalid state: A login callback was received, but no login request was sent. " )
501
- }
502
- guard let appleIDToken = appleIDCredential. identityToken else {
503
- print ( " Unable to fetch identity token " )
504
- return
505
- }
506
- guard let idTokenString = String ( data: appleIDToken, encoding: . utf8) else {
507
- print ( " Unable to serialize token string from data: \( appleIDToken. debugDescription) " )
508
- return
535
+ let authorizationController = ASAuthorizationController ( authorizationRequests: [ request] )
536
+ authorizationController. delegate = self
537
+ authorizationController. performRequests ( )
509
538
}
539
+ }
510
540
511
- let credential = OAuthProvider . credential ( providerID: . apple,
512
- idToken: idTokenString,
513
- rawNonce: nonce)
514
- // Once we have created the above `credential`, we can link accounts to it.
515
- linkAccount ( authCredential: credential)
541
+ func authorizationController( controller: ASAuthorizationController ,
542
+ didCompleteWithAuthorization authorization: ASAuthorization ) {
543
+ if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization. credential {
544
+ continuation? . resume ( returning: appleIDCredential)
545
+ } else {
546
+ fatalError ( " Unexpected authorization credential type. " )
547
+ }
516
548
}
517
549
518
550
func authorizationController( controller: ASAuthorizationController ,
519
551
didCompleteWithError error: any Error ) {
520
552
// Ensure that you have:
521
553
// - enabled `Sign in with Apple` on the Firebase console
522
554
// - added the `Sign in with Apple` capability for this project
523
- print ( " Sign in with Apple errored: \( error) " )
555
+ continuation ? . resume ( throwing : error)
524
556
}
525
557
526
558
// MARK: ASAuthorizationControllerPresentationContextProviding
0 commit comments