-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathAuthNotificationManager.swift
More file actions
166 lines (145 loc) · 6 KB
/
AuthNotificationManager.swift
File metadata and controls
166 lines (145 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// Copyright 2023 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.
#if !os(macOS) && !os(watchOS)
import Foundation
import UIKit
/// A class represents a credential that proves the identity of the app.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
@preconcurrency
class AuthNotificationManager {
/// The key to locate payload data in the remote notification.
private let kNotificationDataKey = "com.google.firebase.auth"
/// The key for the receipt in the remote notification payload data.
private let kNotificationReceiptKey = "receipt"
/// The key for the secret in the remote notification payload data.
private let kNotificationSecretKey = "secret"
/// The key for marking the prober in the remote notification payload data.
private let kNotificationProberKey = "warning"
/// Timeout for probing whether the app delegate forwards the remote notification to us.
private let kProbingTimeout = 15.0
/// The application.
private let application: UIApplication
/// The object to handle app credentials delivered via notification.
private let appCredentialManager: AuthAppCredentialManager
/// Whether notification forwarding has been checked or not.
private var hasCheckedNotificationForwarding: Bool = false
/// Whether or not notification is being forwarded
private var isNotificationBeingForwarded: Bool = false
/// The timeout for checking for notification forwarding.
///
/// Only tests should access this property.
let timeout: TimeInterval
/// Disable callback waiting for tests.
///
/// Only tests should access this property.
var immediateCallbackForTestFaking: (() -> Bool)?
private let condition: AuthCondition
/// Initializes the instance.
/// - Parameter application: The application.
/// - Parameter appCredentialManager: The object to handle app credentials delivered via
/// notification.
/// - Returns: The initialized instance.
init(withApplication application: UIApplication,
appCredentialManager: AuthAppCredentialManager) {
self.application = application
self.appCredentialManager = appCredentialManager
timeout = kProbingTimeout
condition = AuthCondition()
}
private actor PendingCount {
private var count = 0
func increment() -> Int {
count = count + 1
return count
}
}
private let pendingCount = PendingCount()
/// Checks whether or not remote notifications are being forwarded to this class.
func checkNotificationForwarding() async -> Bool {
if let getValueFunc = immediateCallbackForTestFaking {
return getValueFunc()
}
if hasCheckedNotificationForwarding {
return isNotificationBeingForwarded
}
if await pendingCount.increment() == 1 {
Task { @MainActor in
let proberNotification = [self.kNotificationDataKey: [self.kNotificationProberKey:
"This fake notification should be forwarded to Firebase Auth."]]
var attempts = 0
var delegateFound = false
while attempts < 10 && !delegateFound {
if let delegate = self.application.delegate,
delegate.responds(
to: #selector(UIApplicationDelegate
.application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
) {
delegate.application?(
self.application,
didReceiveRemoteNotification: proberNotification
) { _ in }
delegateFound = true
} else {
attempts += 1
try? await Task.sleep(nanoseconds: 300_000_000)
}
}
if !delegateFound {
self.isNotificationBeingForwarded = true
self.condition.signal()
}
kAuthGlobalWorkQueue.asyncAfter(deadline: .now() + .seconds(Int(self.timeout))) {
self.condition.signal()
}
}
}
await condition.wait()
hasCheckedNotificationForwarding = true
return isNotificationBeingForwarded
}
/// Attempts to handle the remote notification.
/// - Parameter notification: The notification in question.
/// - Returns: Whether or the notification has been handled.
func canHandle(notification: [AnyHashable: Any]) -> Bool {
var stringDictionary: [String: Any]?
let data = notification[kNotificationDataKey]
if let jsonString = data as? String {
// Deserialize in case the data is a JSON string.
guard let jsonData = jsonString.data(using: .utf8),
let dictionary = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
else {
return false
}
stringDictionary = dictionary
}
guard let dictionary = stringDictionary ?? data as? [String: Any] else {
return false
}
if dictionary[kNotificationProberKey] != nil {
if hasCheckedNotificationForwarding {
// The prober notification probably comes from another instance, so pass it along.
return false
}
isNotificationBeingForwarded = true
condition.signal()
return true
}
guard let receipt = dictionary[kNotificationReceiptKey] as? String,
let secret = dictionary[kNotificationSecretKey] as? String else {
return false
}
return appCredentialManager.canFinishVerification(withReceipt: receipt, secret: secret)
}
}
#endif