feat(ios): implement full AdMob plugin
Replace the iOS template with a complete Swift plugin mirroring the Android implementation: - SPM Package.swift pulling in GoogleMobileAds (v12) and the Tauri API - Banner, Interstitial, Rewarded and RewardedInterstitial ad types - adCreate/adLoad/adShow/adHide/adIsLoaded command surface - configure/configRequest, UMP consent (isPrivacyOptionsRequired, showPrivacyOptionsForm) and App Tracking Transparency commands - event names kept in sync with the Android Generated.Events constants Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seto committed
Jun 20, 2026 at 05:03 UTC
9206e89d07df9412e5f1dab49fe80f2a4f922a88
10 files changed
+765
ios/Package.swift
new
+39
@@ -0,0 +1,39 @@
1
+// swift-tools-version:5.3
2
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
3
+// SPDX-License-Identifier: Apache-2.0
4
+// SPDX-License-Identifier: MIT
5
+
6
+import PackageDescription
7
+
8
+let package = Package(
9
+ name: "tauri-plugin-admob",
10
+ platforms: [
11
+ .iOS(.v13)
12
+ ],
13
+ products: [
14
+ // Products define the executables and libraries a package produces, making them visible to other packages.
15
+ .library(
16
+ name: "tauri-plugin-admob",
17
+ type: .static,
18
+ targets: ["tauri-plugin-admob"])
19
+ ],
20
+ dependencies: [
21
+ .package(name: "Tauri", path: "../.tauri/tauri-api"),
22
+ .package(
23
+ url: "https://github.com/googleads/swift-package-manager-google-mobile-ads.git",
24
+ from: "12.0.0"),
25
+ ],
26
+ targets: [
27
+ // Targets are the basic building blocks of a package, defining a module or a test suite.
28
+ // Targets can depend on other targets in this package and products from dependencies.
29
+ .target(
30
+ name: "tauri-plugin-admob",
31
+ dependencies: [
32
+ .byName(name: "Tauri"),
33
+ .product(
34
+ name: "GoogleMobileAds",
35
+ package: "swift-package-manager-google-mobile-ads"),
36
+ ],
37
+ path: "Sources/tauri-plugin-admob")
38
+ ]
39
+)
ios/README.md
new
+33
@@ -0,0 +1,33 @@
1
+# Tauri Plugin admob — iOS
2
+
3
+AdMob support for iOS, implemented on top of the [Google Mobile Ads SDK][gma]
4
+via Swift Package Manager.
5
+
6
+## Supported ad types
7
+
8
+- Banner
9
+- Interstitial
10
+- Rewarded
11
+- Rewarded interstitial
12
+
13
+These mirror the Android implementation and emit the same events to the JS side.
14
+
15
+## Consent & privacy
16
+
17
+On startup the plugin initializes the Mobile Ads SDK and requests consent
18
+information through the [User Messaging Platform][ump], presenting the consent
19
+form when required. The `isPrivacyOptionsRequired` and `showPrivacyOptionsForm`
20
+commands let the app surface the privacy options form on demand.
21
+
22
+App Tracking Transparency is exposed through `requestTrackingAuthorization` and
23
+`trackingAuthorizationStatus`.
24
+
25
+## App configuration
26
+
27
+The consuming app must declare its AdMob application id and (for iOS 14+) the
28
+`NSUserTrackingUsageDescription` string in its `Info.plist`, and add
29
+`GADApplicationIdentifier`. See the [Get started guide][getstarted].
30
+
31
+[gma]: https://developers.google.com/admob/ios/quick-start
32
+[ump]: https://developers.google.com/admob/ios/privacy
33
+[getstarted]: https://developers.google.com/admob/ios/quick-start
ios/Sources/tauri-plugin-admob/Ad.swift
new
+67
@@ -0,0 +1,67 @@
1
+import Foundation
2
+import Tauri
3
+import GoogleMobileAds
4
+
5
+/// Base class shared by every ad type. Holds the identity of the ad and a weak
6
+/// reference to the owning plugin used to emit events. Mirrors the Android
7
+/// `core.Ad` / `ads.AdBase` hierarchy.
8
+class AdBase: NSObject {
9
+ let id: Int
10
+ let adUnitId: String
11
+ weak var plugin: AdmobPlugin?
12
+
13
+ init(id: Int, adUnitId: String, plugin: AdmobPlugin) {
14
+ self.id = id
15
+ self.adUnitId = adUnitId
16
+ self.plugin = plugin
17
+ super.init()
18
+ }
19
+
20
+ /// Whether the ad has been loaded and is ready to be shown.
21
+ var isLoaded: Bool { false }
22
+
23
+ func load(args: InvokeArgs, request: Request, invoke: Invoke) {
24
+ invoke.reject("Not implemented")
25
+ }
26
+
27
+ func show(invoke: Invoke) {
28
+ invoke.reject("Not implemented")
29
+ }
30
+
31
+ func hide(invoke: Invoke) {
32
+ invoke.reject("Not implemented")
33
+ }
34
+
35
+ func destroy() {}
36
+
37
+ // MARK: - Event helpers
38
+
39
+ /// Emits an event with the given payload, always including the `adId` so JS
40
+ /// listeners can correlate the event with the originating ad instance.
41
+ func emit(_ event: String, _ data: JSObject = [:]) {
42
+ var payload = data
43
+ payload["adId"] = id
44
+ plugin?.trigger(event, data: payload)
45
+ }
46
+
47
+ /// Emits an event carrying error information, mirroring the Android
48
+ /// `emit(eventName, AdError)` overload.
49
+ func emit(_ event: String, error: Error) {
50
+ let nsError = error as NSError
51
+ emit(event, [
52
+ "code": nsError.code,
53
+ "message": nsError.localizedDescription,
54
+ "cause": nsError.domain,
55
+ ])
56
+ }
57
+
58
+ /// Emits a reward event, mirroring the Android `emit(eventName, RewardItem)` overload.
59
+ func emit(_ event: String, reward: AdReward) {
60
+ emit(event, [
61
+ "reward": [
62
+ "amount": reward.amount.doubleValue,
63
+ "type": reward.type,
64
+ ] as JSObject
65
+ ])
66
+ }
67
+}
ios/Sources/tauri-plugin-admob/AdmobPlugin.swift
new
+228
@@ -0,0 +1,228 @@
1
+import AppTrackingTransparency
2
+import GoogleMobileAds
3
+import SwiftRs
4
+import Tauri
5
+import UIKit
6
+import UserMessagingPlatform
7
+import WebKit
8
+
9
+class AdmobPlugin: Plugin {
10
+ private var webView: WKWebView?
11
+ private var ads: [Int: AdBase] = [:]
12
+ private var bannerContainerConstraints: [NSLayoutConstraint] = []
13
+
14
+ // MARK: - Lifecycle
15
+
16
+ override func load(webview: WKWebView) {
17
+ super.load(webview: webview)
18
+ self.webView = webview
19
+
20
+ MobileAds.shared.start(completionHandler: nil)
21
+
22
+ let parameters = RequestParameters()
23
+ ConsentInformation.shared.requestConsentInfoUpdate(with: parameters) { [weak self] error in
24
+ if let error = error {
25
+ NSLog("admob: consent info update failed: \(error.localizedDescription)")
26
+ return
27
+ }
28
+ guard let vc = self?.topViewController else { return }
29
+ ConsentForm.loadAndPresentIfRequired(from: vc) { formError in
30
+ if let formError = formError {
31
+ NSLog("admob: consent form failed: \(formError.localizedDescription)")
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ // MARK: - Privacy / consent
38
+
39
+ private var isPrivacyOptionsRequired: Bool {
40
+ ConsentInformation.shared.privacyOptionsRequirementStatus == .required
41
+ }
42
+
43
+ @objc public func isPrivacyOptionsRequired(_ invoke: Invoke) {
44
+ invoke.resolve(["isPrivacyOptionsRequired": isPrivacyOptionsRequired])
45
+ }
46
+
47
+ @objc public func showPrivacyOptionsForm(_ invoke: Invoke) {
48
+ DispatchQueue.main.async { [weak self] in
49
+ guard let vc = self?.topViewController else {
50
+ invoke.reject("no view controller available")
51
+ return
52
+ }
53
+ ConsentForm.presentPrivacyOptionsForm(from: vc) { error in
54
+ if let error = error {
55
+ invoke.reject(error.localizedDescription)
56
+ return
57
+ }
58
+ invoke.resolve()
59
+ }
60
+ }
61
+ }
62
+
63
+ // MARK: - App Tracking Transparency
64
+
65
+ @objc public func trackingAuthorizationStatus(_ invoke: Invoke) {
66
+ if #available(iOS 14, *) {
67
+ let status = ATTrackingManager.trackingAuthorizationStatus
68
+ invoke.resolve(["status": status == .authorized])
69
+ } else {
70
+ invoke.resolve(["status": true])
71
+ }
72
+ }
73
+
74
+ @objc public func requestTrackingAuthorization(_ invoke: Invoke) {
75
+ if #available(iOS 14, *) {
76
+ ATTrackingManager.requestTrackingAuthorization { status in
77
+ invoke.resolve(["status": status == .authorized])
78
+ }
79
+ } else {
80
+ invoke.resolve(["status": true])
81
+ }
82
+ }
83
+
84
+ // MARK: - Configuration
85
+
86
+ @objc public func configure(_ invoke: Invoke) throws {
87
+ let args = try invoke.parseArgs(InvokeArgs.self)
88
+ if let muted = args.appMuted {
89
+ MobileAds.shared.isAppMuted = muted
90
+ }
91
+ if let volume = args.appVolume {
92
+ MobileAds.shared.applicationVolume = volume
93
+ }
94
+ args.applyRequestConfiguration()
95
+ invoke.resolve()
96
+ }
97
+
98
+ @objc public func configRequest(_ invoke: Invoke) throws {
99
+ let args = try invoke.parseArgs(InvokeArgs.self)
100
+ args.applyRequestConfiguration()
101
+ invoke.resolve()
102
+ }
103
+
104
+ // MARK: - Ad lifecycle commands
105
+
106
+ @objc public func adCreate(_ invoke: Invoke) throws {
107
+ let args = try invoke.parseArgs(InvokeArgs.self)
108
+ guard let id = args.id else {
109
+ invoke.reject("ad id is missing")
110
+ return
111
+ }
112
+ guard let cls = args.cls else {
113
+ invoke.reject("ad cls is missing")
114
+ return
115
+ }
116
+ guard let adUnitId = args.adUnitId else {
117
+ invoke.reject("ad adUnitId is missing")
118
+ return
119
+ }
120
+
121
+ DispatchQueue.main.async { [weak self] in
122
+ guard let self = self else { return }
123
+ let ad: AdBase
124
+ switch cls {
125
+ case "BannerAd":
126
+ ad = Banner(
127
+ id: id, adUnitId: adUnitId, plugin: self, position: args.position ?? "bottom")
128
+ case "InterstitialAd":
129
+ ad = Interstitial(id: id, adUnitId: adUnitId, plugin: self)
130
+ case "RewardedAd":
131
+ ad = Rewarded(id: id, adUnitId: adUnitId, plugin: self)
132
+ case "RewardedInterstitialAd":
133
+ ad = RewardedInterstitial(id: id, adUnitId: adUnitId, plugin: self)
134
+ default:
135
+ invoke.reject("ad cls is not supported: \(cls)")
136
+ return
137
+ }
138
+ self.ads[id] = ad
139
+ invoke.resolve()
140
+ }
141
+ }
142
+
143
+ @objc public func adIsLoaded(_ invoke: Invoke) throws {
144
+ let args = try invoke.parseArgs(InvokeArgs.self)
145
+ DispatchQueue.main.async { [weak self] in
146
+ guard let ad = self?.ad(for: args.id, invoke: invoke) else { return }
147
+ invoke.resolve(["data": ad.isLoaded])
148
+ }
149
+ }
150
+
151
+ @objc public func adLoad(_ invoke: Invoke) throws {
152
+ let args = try invoke.parseArgs(InvokeArgs.self)
153
+ DispatchQueue.main.async { [weak self] in
154
+ guard let ad = self?.ad(for: args.id, invoke: invoke) else { return }
155
+ ad.load(args: args, request: args.buildRequest(), invoke: invoke)
156
+ }
157
+ }
158
+
159
+ @objc public func adShow(_ invoke: Invoke) throws {
160
+ let args = try invoke.parseArgs(InvokeArgs.self)
161
+ DispatchQueue.main.async { [weak self] in
162
+ guard let ad = self?.ad(for: args.id, invoke: invoke) else { return }
163
+ if ad.isLoaded {
164
+ ad.show(invoke: invoke)
165
+ } else {
166
+ invoke.reject("ad is not loaded")
167
+ }
168
+ }
169
+ }
170
+
171
+ @objc public func adHide(_ invoke: Invoke) throws {
172
+ let args = try invoke.parseArgs(InvokeArgs.self)
173
+ DispatchQueue.main.async { [weak self] in
174
+ guard let ad = self?.ad(for: args.id, invoke: invoke) else { return }
175
+ ad.hide(invoke: invoke)
176
+ }
177
+ }
178
+
179
+ // MARK: - Helpers
180
+
181
+ private func ad(for id: Int?, invoke: Invoke) -> AdBase? {
182
+ guard let id = id, let ad = ads[id] else {
183
+ invoke.reject("Ad not found")
184
+ return nil
185
+ }
186
+ return ad
187
+ }
188
+
189
+ /// Returns the top-most view controller, used to present full screen ads,
190
+ /// consent forms and to host banner views.
191
+ var topViewController: UIViewController? {
192
+ let keyWindow =
193
+ UIApplication.shared.connectedScenes
194
+ .compactMap { $0 as? UIWindowScene }
195
+ .flatMap { $0.windows }
196
+ .first { $0.isKeyWindow }
197
+ var top = keyWindow?.rootViewController
198
+ while let presented = top?.presentedViewController {
199
+ top = presented
200
+ }
201
+ return top
202
+ }
203
+
204
+ /// Attaches a banner view to the root view, pinned to the top or bottom.
205
+ func attachBanner(_ bannerView: BannerView, top: Bool) {
206
+ guard let container = topViewController?.view else { return }
207
+ if bannerView.superview !== container {
208
+ bannerView.removeFromSuperview()
209
+ NSLayoutConstraint.deactivate(bannerContainerConstraints)
210
+ bannerView.translatesAutoresizingMaskIntoConstraints = false
211
+ container.addSubview(bannerView)
212
+ let guide = container.safeAreaLayoutGuide
213
+ bannerContainerConstraints = [
214
+ bannerView.centerXAnchor.constraint(equalTo: guide.centerXAnchor),
215
+ top
216
+ ? bannerView.topAnchor.constraint(equalTo: guide.topAnchor)
217
+ : bannerView.bottomAnchor.constraint(equalTo: guide.bottomAnchor),
218
+ ]
219
+ NSLayoutConstraint.activate(bannerContainerConstraints)
220
+ }
221
+ container.bringSubviewToFront(bannerView)
222
+ }
223
+}
224
+
225
+@_cdecl("init_plugin_admob")
226
+func initPlugin() -> Plugin {
227
+ return AdmobPlugin()
228
+}
ios/Sources/tauri-plugin-admob/Args.swift
new
+75
@@ -0,0 +1,75 @@
1
+import Foundation
2
+import GoogleMobileAds
3
+
4
+/// Decoded representation of the arguments sent from JS for every command.
5
+/// Mirrors the Android `InvokeArgs`/`ServerSideVerification` classes.
6
+struct ServerSideVerificationArgs: Decodable {
7
+ var userId: String?
8
+ var customData: String?
9
+}
10
+
11
+struct InvokeArgs: Decodable {
12
+ var cls: String?
13
+ var id: Int?
14
+ var adUnitId: String?
15
+ var appMuted: Bool?
16
+ var appVolume: Float?
17
+ var position: String?
18
+ var contentUrl: String?
19
+ var npa: String?
20
+ var maxAdContentRating: String?
21
+ var tagForChildDirectedTreatment: Bool?
22
+ var tagForUnderAgeOfConsent: Bool?
23
+ var testDeviceIds: [String]?
24
+ var serverSideVerification: ServerSideVerificationArgs?
25
+ var customData: String?
26
+ var userId: String?
27
+}
28
+
29
+extension InvokeArgs {
30
+ /// Builds an ad `Request` from the optional `contentUrl`/`npa` arguments,
31
+ /// matching `Context.optAdRequest()` on Android.
32
+ func buildRequest() -> Request {
33
+ let request = Request()
34
+ if let contentUrl = contentUrl {
35
+ request.contentURL = contentUrl
36
+ }
37
+ if let npa = npa {
38
+ let extras = Extras()
39
+ extras.additionalParameters = ["npa": npa]
40
+ request.register(extras)
41
+ }
42
+ return request
43
+ }
44
+
45
+ /// Server side verification options for rewarded ads, or nil when not provided.
46
+ func buildServerSideVerificationOptions() -> ServerSideVerificationOptions? {
47
+ guard let ssv = serverSideVerification else { return nil }
48
+ let options = ServerSideVerificationOptions()
49
+ if let userId = ssv.userId {
50
+ options.userIdentifier = userId
51
+ }
52
+ if let customData = ssv.customData {
53
+ options.customRewardString = customData
54
+ }
55
+ return options
56
+ }
57
+
58
+ /// Applies the request configuration arguments to the global `MobileAds`
59
+ /// request configuration, matching `Context.optRequestConfiguration()`.
60
+ func applyRequestConfiguration() {
61
+ let config = MobileAds.shared.requestConfiguration
62
+ if let rating = maxAdContentRating {
63
+ config.maxAdContentRating = MaxAdContentRating(rawValue: rating)
64
+ }
65
+ if let child = tagForChildDirectedTreatment {
66
+ config.tagForChildDirectedTreatment = NSNumber(value: child)
67
+ }
68
+ if let underAge = tagForUnderAgeOfConsent {
69
+ config.tagForUnderAgeOfConsent = NSNumber(value: underAge)
70
+ }
71
+ if let ids = testDeviceIds {
72
+ config.testDeviceIdentifiers = ids
73
+ }
74
+ }
75
+}
ios/Sources/tauri-plugin-admob/Banner.swift
new
+78
@@ -0,0 +1,78 @@
1
+import Foundation
2
+import Tauri
3
+import UIKit
4
+import GoogleMobileAds
5
+
6
+/// Banner ad, mirroring the Android `ads.Banner` implementation. The banner is
7
+/// pinned to the top or bottom of the root view controller's view.
8
+class Banner: AdBase, BannerViewDelegate {
9
+ private var bannerView: BannerView?
10
+ private let position: String
11
+
12
+ init(id: Int, adUnitId: String, plugin: AdmobPlugin, position: String) {
13
+ self.position = position
14
+ super.init(id: id, adUnitId: adUnitId, plugin: plugin)
15
+ }
16
+
17
+ override var isLoaded: Bool { bannerView != nil }
18
+
19
+ override func load(args: InvokeArgs, request: Request, invoke: Invoke) {
20
+ if bannerView == nil {
21
+ let width = UIScreen.main.bounds.width
22
+ let bannerView = BannerView(
23
+ adSize: currentOrientationAnchoredAdaptiveBanner(width: width))
24
+ bannerView.adUnitID = adUnitId
25
+ bannerView.rootViewController = plugin?.topViewController
26
+ bannerView.delegate = self
27
+ self.bannerView = bannerView
28
+ }
29
+ bannerView?.load(request)
30
+ invoke.resolve()
31
+ }
32
+
33
+ override func show(invoke: Invoke) {
34
+ guard let bannerView = bannerView, let plugin = plugin else {
35
+ invoke.reject("banner ad is not loaded")
36
+ return
37
+ }
38
+ plugin.attachBanner(bannerView, top: position == "top")
39
+ bannerView.isHidden = false
40
+ invoke.resolve()
41
+ }
42
+
43
+ override func hide(invoke: Invoke) {
44
+ bannerView?.isHidden = true
45
+ invoke.resolve()
46
+ }
47
+
48
+ override func destroy() {
49
+ bannerView?.removeFromSuperview()
50
+ bannerView = nil
51
+ }
52
+
53
+ // MARK: - BannerViewDelegate
54
+
55
+ func bannerViewDidReceiveAd(_ bannerView: BannerView) {
56
+ emit(Events.bannerLoad)
57
+ }
58
+
59
+ func bannerView(_ bannerView: BannerView, didFailToReceiveAdWithError error: Error) {
60
+ emit(Events.bannerLoadFail, error: error)
61
+ }
62
+
63
+ func bannerViewDidRecordImpression(_ bannerView: BannerView) {
64
+ emit(Events.bannerImpression)
65
+ }
66
+
67
+ func bannerViewDidRecordClick(_ bannerView: BannerView) {
68
+ emit(Events.bannerClick)
69
+ }
70
+
71
+ func bannerViewWillPresentScreen(_ bannerView: BannerView) {
72
+ emit(Events.bannerOpen)
73
+ }
74
+
75
+ func bannerViewDidDismissScreen(_ bannerView: BannerView) {
76
+ emit(Events.bannerClose)
77
+ }
78
+}
ios/Sources/tauri-plugin-admob/Generated.swift
new
+35
@@ -0,0 +1,35 @@
1
+import Foundation
2
+
3
+/// Event names emitted to the JS side. Kept in sync with the Android
4
+/// `Generated.Events` constants so listeners behave identically on both platforms.
5
+enum Events {
6
+ static let bannerClick = "banner_click"
7
+ static let bannerClose = "banner_close"
8
+ static let bannerImpression = "banner_impression"
9
+ static let bannerLoad = "banner_load"
10
+ static let bannerLoadFail = "banner_load_fail"
11
+ static let bannerOpen = "banner_open"
12
+
13
+ static let interstitialDismiss = "interstitial_dismiss"
14
+ static let interstitialImpression = "interstitial_impression"
15
+ static let interstitialLoad = "interstitial_load"
16
+ static let interstitialLoadFail = "interstitial_load_fail"
17
+ static let interstitialShow = "interstitial_show"
18
+ static let interstitialShowFail = "interstitial_show_fail"
19
+
20
+ static let rewardedInterstitialDismiss = "rewardedInterstitial_dismiss"
21
+ static let rewardedInterstitialImpression = "rewardedInterstitial_impression"
22
+ static let rewardedInterstitialLoad = "rewardedInterstitial_load"
23
+ static let rewardedInterstitialLoadFail = "rewardedInterstitial_load_fail"
24
+ static let rewardedInterstitialReward = "rewardedInterstitial_reward"
25
+ static let rewardedInterstitialShow = "rewardedInterstitial_show"
26
+ static let rewardedInterstitialShowFail = "rewardedInterstitial_show_fail"
27
+
28
+ static let rewardedDismiss = "rewarded_dismiss"
29
+ static let rewardedImpression = "rewarded_impression"
30
+ static let rewardedLoad = "rewarded_load"
31
+ static let rewardedLoadFail = "rewarded_load_fail"
32
+ static let rewardedReward = "rewarded_reward"
33
+ static let rewardedShow = "rewarded_show"
34
+ static let rewardedShowFail = "rewarded_show_fail"
35
+}
ios/Sources/tauri-plugin-admob/Interstitial.swift
new
+66
@@ -0,0 +1,66 @@
1
+import Foundation
2
+import Tauri
3
+import GoogleMobileAds
4
+
5
+/// Interstitial ad, mirroring the Android `ads.Interstitial` implementation.
6
+class Interstitial: AdBase, FullScreenContentDelegate {
7
+ private var ad: InterstitialAd?
8
+
9
+ override var isLoaded: Bool { ad != nil }
10
+
11
+ override func load(args: InvokeArgs, request: Request, invoke: Invoke) {
12
+ clear()
13
+ InterstitialAd.load(with: adUnitId, request: request) { [weak self] ad, error in
14
+ guard let self = self else { return }
15
+ if let error = error {
16
+ self.clear()
17
+ self.emit(Events.interstitialLoadFail, error: error)
18
+ invoke.reject(error.localizedDescription)
19
+ return
20
+ }
21
+ self.ad = ad
22
+ ad?.fullScreenContentDelegate = self
23
+ self.emit(Events.interstitialLoad)
24
+ invoke.resolve()
25
+ }
26
+ }
27
+
28
+ override func show(invoke: Invoke) {
29
+ guard let ad = ad, let vc = plugin?.topViewController else {
30
+ invoke.reject("interstitial ad is not loaded")
31
+ return
32
+ }
33
+ ad.present(from: vc)
34
+ invoke.resolve()
35
+ }
36
+
37
+ override func destroy() {
38
+ clear()
39
+ }
40
+
41
+ private func clear() {
42
+ ad?.fullScreenContentDelegate = nil
43
+ ad = nil
44
+ }
45
+
46
+ // MARK: - FullScreenContentDelegate
47
+
48
+ func adDidRecordImpression(_ ad: FullScreenPresentingAd) {
49
+ emit(Events.interstitialImpression)
50
+ }
51
+
52
+ func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
53
+ emit(Events.interstitialShow)
54
+ }
55
+
56
+ func ad(
57
+ _ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error
58
+ ) {
59
+ emit(Events.interstitialShowFail, error: error)
60
+ }
61
+
62
+ func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
63
+ clear()
64
+ emit(Events.interstitialDismiss)
65
+ }
66
+}
ios/Sources/tauri-plugin-admob/Rewarded.swift
new
+72
@@ -0,0 +1,72 @@
1
+import Foundation
2
+import Tauri
3
+import GoogleMobileAds
4
+
5
+/// Rewarded ad, mirroring the Android `ads.Rewarded` implementation.
6
+class Rewarded: AdBase, FullScreenContentDelegate {
7
+ private var ad: RewardedAd?
8
+
9
+ override var isLoaded: Bool { ad != nil }
10
+
11
+ override func load(args: InvokeArgs, request: Request, invoke: Invoke) {
12
+ clear()
13
+ RewardedAd.load(with: adUnitId, request: request) { [weak self] ad, error in
14
+ guard let self = self else { return }
15
+ if let error = error {
16
+ self.clear()
17
+ self.emit(Events.rewardedLoadFail, error: error)
18
+ invoke.reject(error.localizedDescription)
19
+ return
20
+ }
21
+ self.ad = ad
22
+ if let ssv = args.buildServerSideVerificationOptions() {
23
+ ad?.serverSideVerificationOptions = ssv
24
+ }
25
+ ad?.fullScreenContentDelegate = self
26
+ self.emit(Events.rewardedLoad)
27
+ invoke.resolve()
28
+ }
29
+ }
30
+
31
+ override func show(invoke: Invoke) {
32
+ guard let ad = ad, let vc = plugin?.topViewController else {
33
+ invoke.reject("rewarded ad is not loaded")
34
+ return
35
+ }
36
+ ad.present(from: vc) { [weak self, weak ad] in
37
+ guard let self = self, let ad = ad else { return }
38
+ self.emit(Events.rewardedReward, reward: ad.adReward)
39
+ }
40
+ invoke.resolve()
41
+ }
42
+
43
+ override func destroy() {
44
+ clear()
45
+ }
46
+
47
+ private func clear() {
48
+ ad?.fullScreenContentDelegate = nil
49
+ ad = nil
50
+ }
51
+
52
+ // MARK: - FullScreenContentDelegate
53
+
54
+ func adDidRecordImpression(_ ad: FullScreenPresentingAd) {
55
+ emit(Events.rewardedImpression)
56
+ }
57
+
58
+ func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
59
+ emit(Events.rewardedShow)
60
+ }
61
+
62
+ func ad(
63
+ _ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error
64
+ ) {
65
+ emit(Events.rewardedShowFail, error: error)
66
+ }
67
+
68
+ func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
69
+ clear()
70
+ emit(Events.rewardedDismiss)
71
+ }
72
+}
ios/Sources/tauri-plugin-admob/RewardedInterstitial.swift
new
+72
@@ -0,0 +1,72 @@
1
+import Foundation
2
+import Tauri
3
+import GoogleMobileAds
4
+
5
+/// Rewarded interstitial ad, mirroring the Android `ads.RewardedInterstitial`.
6
+class RewardedInterstitial: AdBase, FullScreenContentDelegate {
7
+ private var ad: RewardedInterstitialAd?
8
+
9
+ override var isLoaded: Bool { ad != nil }
10
+
11
+ override func load(args: InvokeArgs, request: Request, invoke: Invoke) {
12
+ clear()
13
+ RewardedInterstitialAd.load(with: adUnitId, request: request) { [weak self] ad, error in
14
+ guard let self = self else { return }
15
+ if let error = error {
16
+ self.clear()
17
+ self.emit(Events.rewardedInterstitialLoadFail, error: error)
18
+ invoke.reject(error.localizedDescription)
19
+ return
20
+ }
21
+ self.ad = ad
22
+ if let ssv = args.buildServerSideVerificationOptions() {
23
+ ad?.serverSideVerificationOptions = ssv
24
+ }
25
+ ad?.fullScreenContentDelegate = self
26
+ self.emit(Events.rewardedInterstitialLoad)
27
+ invoke.resolve()
28
+ }
29
+ }
30
+
31
+ override func show(invoke: Invoke) {
32
+ guard let ad = ad, let vc = plugin?.topViewController else {
33
+ invoke.reject("rewarded interstitial ad is not loaded")
34
+ return
35
+ }
36
+ ad.present(from: vc) { [weak self, weak ad] in
37
+ guard let self = self, let ad = ad else { return }
38
+ self.emit(Events.rewardedInterstitialReward, reward: ad.adReward)
39
+ }
40
+ invoke.resolve()
41
+ }
42
+
43
+ override func destroy() {
44
+ clear()
45
+ }
46
+
47
+ private func clear() {
48
+ ad?.fullScreenContentDelegate = nil
49
+ ad = nil
50
+ }
51
+
52
+ // MARK: - FullScreenContentDelegate
53
+
54
+ func adDidRecordImpression(_ ad: FullScreenPresentingAd) {
55
+ emit(Events.rewardedInterstitialImpression)
56
+ }
57
+
58
+ func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
59
+ emit(Events.rewardedInterstitialShow)
60
+ }
61
+
62
+ func ad(
63
+ _ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error
64
+ ) {
65
+ emit(Events.rewardedInterstitialShowFail, error: error)
66
+ }
67
+
68
+ func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
69
+ clear()
70
+ emit(Events.rewardedInterstitialDismiss)
71
+ }
72
+}