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 +}