-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGmaWebViewManager.swift
More file actions
270 lines (232 loc) · 6.96 KB
/
Copy pathGmaWebViewManager.swift
File metadata and controls
270 lines (232 loc) · 6.96 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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import GoogleMobileAds
import React
import SafariServices
import UIKit
import WebKit
/// Starts GMA once and registers every WKWebView before its first URL load.
@MainActor
private enum GmaWebViewRegistration {
private static var isReady = false
private static var isStarting = false
private static var completions: [() -> Void] = []
static func register(_ webView: WKWebView, completion: @escaping () -> Void) {
let registration = {
MobileAds.shared.register(webView)
completion()
}
if isReady {
registration()
return
}
completions.append(registration)
guard !isStarting else { return }
isStarting = true
MobileAds.shared.start { _ in
Task { @MainActor in
isReady = true
isStarting = false
let pending = completions
completions.removeAll()
pending.forEach { $0() }
}
}
}
}
/// React Native manager for the app-owned, GMA-registered WKWebView.
@objc(GmaWebViewManager)
final class GmaWebViewManager: RCTViewManager {
override func view() -> UIView! {
GmaWebView()
}
override static func requiresMainQueueSetup() -> Bool {
true
}
}
/**
* WKWebView configured for ads, safe click-outs, and per-instance GMA
* registration. Host allowlisting is applied only to top-level navigation.
*/
final class GmaWebView: UIView, WKNavigationDelegate, WKUIDelegate {
@objc var sourceUrl: NSString? {
didSet {
guard sourceUrl != oldValue else { return }
pendingSourceUrl = sourceUrl as String?
loadPendingSource()
}
}
@objc var reloadToken: NSNumber = 0 {
didSet {
guard reloadToken != oldValue else { return }
webView.reload()
}
}
@objc var goBackToken: NSNumber = 0 {
didSet {
guard goBackToken != oldValue, webView.canGoBack else { return }
webView.goBack()
}
}
@objc var onLoadProgress: RCTDirectEventBlock?
@objc var onError: RCTDirectEventBlock?
@objc var onNavigationStateChange: RCTDirectEventBlock?
private let webView: WKWebView
private var progressObservation: NSKeyValueObservation?
private var isGmaReady = false
private var pendingSourceUrl: String?
private var currentSourceUrl: String?
override init(frame: CGRect) {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
configuration.websiteDataStore = .default()
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
webView = WKWebView(frame: .zero, configuration: configuration)
super.init(frame: frame)
backgroundColor = .white
webView.backgroundColor = .white
webView.isOpaque = false
webView.navigationDelegate = self
webView.uiDelegate = self
webView.allowsBackForwardNavigationGestures = true
addSubview(webView)
progressObservation = webView.observe(
\.estimatedProgress,
options: [.initial, .new]
) { [weak self] webView, _ in
DispatchQueue.main.async {
self?.emitProgress(webView.estimatedProgress)
}
}
GmaWebViewRegistration.register(webView) { [weak self] in
self?.isGmaReady = true
self?.loadPendingSource()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
webView.frame = bounds
}
private func loadPendingSource() {
guard
isGmaReady,
let source = pendingSourceUrl,
source != currentSourceUrl,
let url = URL(string: source)
else {
return
}
pendingSourceUrl = nil
currentSourceUrl = source
webView.load(URLRequest(url: url))
}
private func emitProgress(_ progress: Double) {
onLoadProgress?(["progress": progress])
}
private func emitNavigationState() {
onNavigationStateChange?(["canGoBack": webView.canGoBack])
}
private func emitError(_ message: String) {
onError?(["message": message])
}
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// A false main-frame flag denotes an iframe. Never apply the top-level
// allowlist to game engines, ad tags, analytics, or about: documents.
if navigationAction.targetFrame?.isMainFrame == false {
decisionHandler(.allow)
return
}
// A nil target frame is target=_blank or window.open(..., "_blank").
if navigationAction.targetFrame == nil {
openClickOut(url)
decisionHandler(.cancel)
return
}
if Self.isAllowedInWebView(url) {
decisionHandler(.allow)
} else {
openClickOut(url)
decisionHandler(.cancel)
}
}
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
if let url = navigationAction.request.url {
openClickOut(url)
}
return nil
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
emitProgress(0)
emitNavigationState()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
emitProgress(1)
emitNavigationState()
}
func webView(
_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error
) {
emitError(error.localizedDescription)
emitNavigationState()
}
private func openClickOut(_ url: URL) {
let scheme = url.scheme?.lowercased() ?? ""
let host = url.host?.lowercased() ?? ""
if !["http", "https"].contains(scheme) ||
host == "apps.apple.com" ||
host == "play.google.com"
{
UIApplication.shared.open(url) { [weak self] opened in
if !opened {
self?.emitError("Could not open \(url.absoluteString)")
}
}
return
}
guard let viewController = nearestViewController else {
emitError("Could not present \(url.absoluteString)")
return
}
viewController.present(SFSafariViewController(url: url), animated: true)
}
private var nearestViewController: UIViewController? {
var responder: UIResponder? = self
while let current = responder {
if let viewController = current as? UIViewController {
return viewController
}
responder = current.next
}
return nil
}
/// Only called for top-level navigation; subframes bypass this policy.
static func isAllowedInWebView(_ url: URL) -> Bool {
let scheme = url.scheme?.lowercased() ?? ""
if ["about", "blob", "data"].contains(scheme) {
return true
}
guard scheme == "https", let host = url.host?.lowercased() else {
return false
}
return host == "google.github.io" ||
host == "gamezop.com" ||
host.hasSuffix(".gamezop.com")
}
}