expo-ads 0.1.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.editorconfig +5 -0
  2. package/android/src/main/java/expo/modules/ads/meta/MetaAdsInitializer.kt +2 -0
  3. package/android/src/main/java/expo/modules/ads/meta/MetaAdsModule.kt +277 -72
  4. package/android/src/main/java/expo/modules/ads/meta/MetaBannerAdView.kt +79 -39
  5. package/android/src/main/java/expo/modules/ads/meta/MetaInterstitialManager.kt +130 -0
  6. package/android/src/main/java/expo/modules/ads/meta/MetaNativeAdManager.kt +146 -0
  7. package/android/src/main/java/expo/modules/ads/meta/MetaNativeAdView.kt +142 -0
  8. package/android/src/main/java/expo/modules/ads/meta/MetaNativeBannerAdType.kt +59 -0
  9. package/android/src/main/java/expo/modules/ads/meta/MetaNativeHostViews.kt +175 -0
  10. package/android/src/main/java/expo/modules/ads/meta/MetaRewardedInterstitialManager.kt +168 -0
  11. package/android/src/main/java/expo/modules/ads/meta/MetaRewardedManager.kt +168 -0
  12. package/build/meta/MetaAdEventType.d.ts +6 -0
  13. package/build/meta/MetaAdEventType.d.ts.map +1 -0
  14. package/build/meta/MetaAdEventType.js +2 -0
  15. package/build/meta/MetaAdEventType.js.map +1 -0
  16. package/build/meta/MetaAdsModule.d.ts +13 -0
  17. package/build/meta/MetaAdsModule.d.ts.map +1 -1
  18. package/build/meta/MetaAdsModule.js.map +1 -1
  19. package/build/meta/ads/BannerAd.d.ts +2 -2
  20. package/build/meta/ads/BannerAd.d.ts.map +1 -1
  21. package/build/meta/ads/BannerAd.js +9 -4
  22. package/build/meta/ads/BannerAd.js.map +1 -1
  23. package/build/meta/ads/InterstitialAd.d.ts +37 -0
  24. package/build/meta/ads/InterstitialAd.d.ts.map +1 -0
  25. package/build/meta/ads/InterstitialAd.js +99 -0
  26. package/build/meta/ads/InterstitialAd.js.map +1 -0
  27. package/build/meta/ads/RewardedAd.d.ts +55 -0
  28. package/build/meta/ads/RewardedAd.d.ts.map +1 -0
  29. package/build/meta/ads/RewardedAd.js +171 -0
  30. package/build/meta/ads/RewardedAd.js.map +1 -0
  31. package/build/meta/ads/RewardedInterstitialAd.d.ts +54 -0
  32. package/build/meta/ads/RewardedInterstitialAd.d.ts.map +1 -0
  33. package/build/meta/ads/RewardedInterstitialAd.js +170 -0
  34. package/build/meta/ads/RewardedInterstitialAd.js.map +1 -0
  35. package/build/meta/ads/native-ad/NativeAd.d.ts +59 -0
  36. package/build/meta/ads/native-ad/NativeAd.d.ts.map +1 -0
  37. package/build/meta/ads/native-ad/NativeAd.js +106 -0
  38. package/build/meta/ads/native-ad/NativeAd.js.map +1 -0
  39. package/build/meta/ads/native-ad/NativeAdView.d.ts +47 -0
  40. package/build/meta/ads/native-ad/NativeAdView.d.ts.map +1 -0
  41. package/build/meta/ads/native-ad/NativeAdView.js +58 -0
  42. package/build/meta/ads/native-ad/NativeAdView.js.map +1 -0
  43. package/build/meta/ads/native-ad/NativeAssets.d.ts +24 -0
  44. package/build/meta/ads/native-ad/NativeAssets.d.ts.map +1 -0
  45. package/build/meta/ads/native-ad/NativeAssets.js +28 -0
  46. package/build/meta/ads/native-ad/NativeAssets.js.map +1 -0
  47. package/build/meta/events/MetaInterstitialAdEventType.d.ts +15 -0
  48. package/build/meta/events/MetaInterstitialAdEventType.d.ts.map +1 -0
  49. package/build/meta/events/MetaInterstitialAdEventType.js +16 -0
  50. package/build/meta/events/MetaInterstitialAdEventType.js.map +1 -0
  51. package/build/meta/events/MetaNativeAdEventType.d.ts +12 -0
  52. package/build/meta/events/MetaNativeAdEventType.d.ts.map +1 -0
  53. package/build/meta/events/MetaNativeAdEventType.js +13 -0
  54. package/build/meta/events/MetaNativeAdEventType.js.map +1 -0
  55. package/build/meta/events/MetaRewardedAdEventType.d.ts +17 -0
  56. package/build/meta/events/MetaRewardedAdEventType.d.ts.map +1 -0
  57. package/build/meta/events/MetaRewardedAdEventType.js +18 -0
  58. package/build/meta/events/MetaRewardedAdEventType.js.map +1 -0
  59. package/build/meta/events/MetaRewardedInterstitialAdEventType.d.ts +13 -0
  60. package/build/meta/events/MetaRewardedInterstitialAdEventType.d.ts.map +1 -0
  61. package/build/meta/events/MetaRewardedInterstitialAdEventType.js +14 -0
  62. package/build/meta/events/MetaRewardedInterstitialAdEventType.js.map +1 -0
  63. package/build/meta/hooks/useInterstitialAd.d.ts +12 -0
  64. package/build/meta/hooks/useInterstitialAd.d.ts.map +1 -0
  65. package/build/meta/hooks/useInterstitialAd.js +48 -0
  66. package/build/meta/hooks/useInterstitialAd.js.map +1 -0
  67. package/build/meta/hooks/useNativeAd.d.ts +37 -0
  68. package/build/meta/hooks/useNativeAd.d.ts.map +1 -0
  69. package/build/meta/hooks/useNativeAd.js +50 -0
  70. package/build/meta/hooks/useNativeAd.js.map +1 -0
  71. package/build/meta/hooks/useRewardedAd.d.ts +16 -0
  72. package/build/meta/hooks/useRewardedAd.d.ts.map +1 -0
  73. package/build/meta/hooks/useRewardedAd.js +83 -0
  74. package/build/meta/hooks/useRewardedAd.js.map +1 -0
  75. package/build/meta/hooks/useRewardedInterstitialAd.d.ts +16 -0
  76. package/build/meta/hooks/useRewardedInterstitialAd.d.ts.map +1 -0
  77. package/build/meta/hooks/useRewardedInterstitialAd.js +83 -0
  78. package/build/meta/hooks/useRewardedInterstitialAd.js.map +1 -0
  79. package/build/meta/index.d.ts +21 -1
  80. package/build/meta/index.d.ts.map +1 -1
  81. package/build/meta/index.js +14 -2
  82. package/build/meta/index.js.map +1 -1
  83. package/build/meta/internal/ref.d.ts +10 -0
  84. package/build/meta/internal/ref.d.ts.map +1 -0
  85. package/build/meta/internal/ref.js +31 -0
  86. package/build/meta/internal/ref.js.map +1 -0
  87. package/build/meta/specs/components/{ExpoAdsMetaBannerViewNativeComponent.d.ts → MetaAdsBannerViewNativeComponent.d.ts} +3 -3
  88. package/build/meta/specs/components/MetaAdsBannerViewNativeComponent.d.ts.map +1 -0
  89. package/build/meta/specs/components/MetaAdsBannerViewNativeComponent.js +5 -0
  90. package/build/meta/specs/components/MetaAdsBannerViewNativeComponent.js.map +1 -0
  91. package/build/meta/specs/components/MetaAdsNativeViewNativeComponent.d.ts +23 -0
  92. package/build/meta/specs/components/MetaAdsNativeViewNativeComponent.d.ts.map +1 -0
  93. package/build/meta/specs/components/MetaAdsNativeViewNativeComponent.js +7 -0
  94. package/build/meta/specs/components/MetaAdsNativeViewNativeComponent.js.map +1 -0
  95. package/build/meta/types/MetaAdError.d.ts +2 -1
  96. package/build/meta/types/MetaAdError.d.ts.map +1 -1
  97. package/build/meta/types/MetaAdError.js.map +1 -1
  98. package/build/meta/types/MetaAdEventListener.d.ts +8 -0
  99. package/build/meta/types/MetaAdEventListener.d.ts.map +1 -0
  100. package/build/meta/types/MetaAdEventListener.js +2 -0
  101. package/build/meta/types/MetaAdEventListener.js.map +1 -0
  102. package/build/meta/types/MetaAdEventsListener.d.ts +7 -0
  103. package/build/meta/types/MetaAdEventsListener.d.ts.map +1 -0
  104. package/build/meta/types/MetaAdEventsListener.js +2 -0
  105. package/build/meta/types/MetaAdEventsListener.js.map +1 -0
  106. package/build/meta/types/MetaBannerAdProps.d.ts +6 -0
  107. package/build/meta/types/MetaBannerAdProps.d.ts.map +1 -1
  108. package/build/meta/types/MetaBannerAdProps.js.map +1 -1
  109. package/build/meta/types/MetaInterstitialEvent.d.ts +8 -0
  110. package/build/meta/types/MetaInterstitialEvent.d.ts.map +1 -0
  111. package/build/meta/types/MetaInterstitialEvent.js +2 -0
  112. package/build/meta/types/MetaInterstitialEvent.js.map +1 -0
  113. package/build/meta/types/MetaNativeAdProps.d.ts +55 -0
  114. package/build/meta/types/MetaNativeAdProps.d.ts.map +1 -0
  115. package/build/meta/types/MetaNativeAdProps.js +13 -0
  116. package/build/meta/types/MetaNativeAdProps.js.map +1 -0
  117. package/build/meta/types/MetaRewardedEvent.d.ts +19 -0
  118. package/build/meta/types/MetaRewardedEvent.d.ts.map +1 -0
  119. package/build/meta/types/MetaRewardedEvent.js +2 -0
  120. package/build/meta/types/MetaRewardedEvent.js.map +1 -0
  121. package/build/meta/types/MetaRewardedInterstitialEvent.d.ts +19 -0
  122. package/build/meta/types/MetaRewardedInterstitialEvent.d.ts.map +1 -0
  123. package/build/meta/types/MetaRewardedInterstitialEvent.js +2 -0
  124. package/build/meta/types/MetaRewardedInterstitialEvent.js.map +1 -0
  125. package/package.json +1 -1
  126. package/src/meta/MetaAdEventType.ts +7 -0
  127. package/src/meta/MetaAdsModule.ts +26 -0
  128. package/src/meta/ads/BannerAd.tsx +12 -4
  129. package/src/meta/ads/InterstitialAd.ts +122 -0
  130. package/src/meta/ads/RewardedAd.ts +198 -0
  131. package/src/meta/ads/RewardedInterstitialAd.ts +206 -0
  132. package/src/meta/ads/native-ad/NativeAd.ts +142 -0
  133. package/src/meta/ads/native-ad/NativeAdView.tsx +88 -0
  134. package/src/meta/ads/native-ad/NativeAssets.tsx +52 -0
  135. package/src/meta/events/MetaInterstitialAdEventType.ts +14 -0
  136. package/src/meta/events/MetaNativeAdEventType.ts +13 -0
  137. package/src/meta/events/MetaRewardedAdEventType.ts +17 -0
  138. package/src/meta/events/MetaRewardedInterstitialAdEventType.ts +12 -0
  139. package/src/meta/hooks/useInterstitialAd.ts +54 -0
  140. package/src/meta/hooks/useNativeAd.ts +60 -0
  141. package/src/meta/hooks/useRewardedAd.ts +100 -0
  142. package/src/meta/hooks/useRewardedInterstitialAd.ts +102 -0
  143. package/src/meta/index.ts +32 -2
  144. package/src/meta/internal/ref.ts +37 -0
  145. package/src/meta/specs/components/{ExpoAdsMetaBannerViewNativeComponent.ts → MetaAdsBannerViewNativeComponent.ts} +6 -3
  146. package/src/meta/specs/components/MetaAdsNativeViewNativeComponent.ts +43 -0
  147. package/src/meta/types/MetaAdError.ts +8 -1
  148. package/src/meta/types/MetaAdEventListener.ts +15 -0
  149. package/src/meta/types/MetaAdEventsListener.ts +7 -0
  150. package/src/meta/types/MetaBannerAdProps.ts +7 -0
  151. package/src/meta/types/MetaInterstitialEvent.ts +8 -0
  152. package/src/meta/types/MetaNativeAdProps.ts +66 -0
  153. package/src/meta/types/MetaRewardedEvent.ts +20 -0
  154. package/src/meta/types/MetaRewardedInterstitialEvent.ts +20 -0
  155. package/tsconfig.json +1 -1
  156. package/build/meta/specs/components/ExpoAdsMetaBannerViewNativeComponent.d.ts.map +0 -1
  157. package/build/meta/specs/components/ExpoAdsMetaBannerViewNativeComponent.js +0 -5
  158. package/build/meta/specs/components/ExpoAdsMetaBannerViewNativeComponent.js.map +0 -1
package/.editorconfig ADDED
@@ -0,0 +1,5 @@
1
+ root = true
2
+
3
+ [*.{kt,kts}]
4
+ indent_style = space
5
+ indent_size = 2
@@ -21,6 +21,8 @@ object MetaAdsInitializer : AudienceNetworkAds.InitListener {
21
21
 
22
22
  fun isReady(): Boolean = synchronized(lock) { state == State.READY }
23
23
 
24
+ fun clearPending() = synchronized(lock) { pending.clear() }
25
+
24
26
  fun whenReady(context: Context, callback: (success: Boolean, message: String) -> Unit) {
25
27
  val appContext = context.applicationContext
26
28
  var startInit = false
@@ -8,90 +8,295 @@ import expo.modules.kotlin.modules.ModuleDefinition
8
8
 
9
9
  class MetaAdsModule : Module() {
10
10
  init {
11
- Log.d("ExpoAdsMeta", "🔥 MetaAdsModule INSTANCE CREATED")
11
+ Log.d("MetaAds", "🔥 MetaAdsModule INSTANCE CREATED")
12
12
  }
13
-
14
- override fun definition() = ModuleDefinition {
15
- Name("MetaAds") // the name JS uses to find this module (import from "expo-ads/meta")
16
-
17
- // Runs automatically when the module loads (warm-up)
18
- OnCreate {
19
- val ctx = appContext.reactContext
20
- if (ctx == null) {
21
- Log.w(MetaAdsInitializer.TAG, "[Module] OnCreate: React context is null, skipping warm-up")
22
- } else {
23
- Log.d(MetaAdsInitializer.TAG, "[Module] OnCreate: starting SDK warm-up")
24
- MetaAdsInitializer.whenReady(ctx) { _, _ -> }
13
+
14
+ override fun definition() =
15
+ ModuleDefinition {
16
+ Name("MetaAds") // the name JS uses to find this module (import from "expo-ads/meta")
17
+
18
+ // Runs automatically when the module loads (warm-up)
19
+ OnCreate {
20
+ val ctx = appContext.reactContext
21
+ if (ctx == null) {
22
+ Log.w(MetaAdsInitializer.TAG, "[Module] OnCreate: React context is null, skipping warm-up")
23
+ } else {
24
+ Log.d(MetaAdsInitializer.TAG, "[Module] OnCreate: starting SDK warm-up")
25
+ MetaAdsInitializer.whenReady(ctx) { _, _ -> }
26
+ }
25
27
  }
26
- }
27
28
 
28
- // Lets JS call: await MetaAds.initialize()
29
- AsyncFunction("initialize") { promise: Promise ->
30
- val ctx = appContext.reactContext
31
- if (ctx == null) {
32
- Log.e(MetaAdsInitializer.TAG, "[Module] initialize() called from JS but React context is null")
33
- promise.reject("ERR_NO_CONTEXT", "React context not available", null)
34
- return@AsyncFunction
29
+ OnDestroy {
30
+ MetaAdsInitializer.clearPending()
31
+ MetaInterstitialManager.destroyAll()
32
+ MetaRewardedInterstitialManager.destroyAll()
35
33
  }
36
- Log.d(MetaAdsInitializer.TAG, "[Module] initialize() called from JS")
37
- MetaAdsInitializer.whenReady(ctx) { ok, msg ->
38
- if (ok) promise.resolve(msg) else promise.reject("ERR_META_INIT", msg, null)
34
+
35
+ Events("onInterstitialEvent", "onRewardedEvent", "onRewardedInterstitialEvent")
36
+
37
+ // Lets JS call: await MetaAds.initialize()
38
+ AsyncFunction("initialize") { promise: Promise ->
39
+ val ctx = appContext.reactContext
40
+ if (ctx == null) {
41
+ Log.e(MetaAdsInitializer.TAG, "[Module] initialize() called from JS but React context is null")
42
+ promise.reject("ERR_NO_CONTEXT", "React context not available", null)
43
+ return@AsyncFunction
44
+ }
45
+ Log.d(MetaAdsInitializer.TAG, "[Module] initialize() called from JS")
46
+ MetaAdsInitializer.whenReady(ctx) { ok, msg ->
47
+ if (ok) promise.resolve(msg) else promise.reject("ERR_META_INIT", msg, null)
48
+ }
39
49
  }
40
- }
41
50
 
42
- // Lets JS call: await MetaAds.setTestMode(true)
43
- //
44
- // This is a plain static AdSettings toggle with no I/O, so it's a synchronous Function
45
- // rather than an AsyncFunction — but it's still safe (and expected) to `await` it from
46
- // JS, since every native function on this module returns a Promise from the JS side
47
- // regardless of whether it's sync or async natively.
48
- //
49
- // Deliberately NOT gated on MetaAdsInitializer.isReady(): Meta's own docs call this
50
- // (and addTestDevices) *before* the SDK finishes async initialization — test mode and
51
- // test devices must be registered before the first loadAd() call, not after init.
52
- Function("setTestMode") { enabled: Boolean ->
53
- Log.d(MetaAdsInitializer.TAG, "[Module] setTestMode($enabled)")
54
- AdSettings.setTestMode(enabled)
55
- }
51
+ // Lets JS call: await MetaAds.setTestMode(true)
52
+ //
53
+ // This is a plain static AdSettings toggle with no I/O, so it's a synchronous Function
54
+ // rather than an AsyncFunction — but it's still safe (and expected) to `await` it from
55
+ // JS, since every native function on this module returns a Promise from the JS side
56
+ // regardless of whether it's sync or async natively.
57
+ //
58
+ // Deliberately NOT gated on MetaAdsInitializer.isReady(): Meta's own docs call this
59
+ // (and addTestDevices) *before* the SDK finishes async initialization — test mode and
60
+ // test devices must be registered before the first loadAd() call, not after init.
61
+ Function("setTestMode") { enabled: Boolean ->
62
+ Log.d(MetaAdsInitializer.TAG, "[Module] setTestMode($enabled)")
63
+ AdSettings.setTestMode(enabled)
64
+ }
56
65
 
57
- // Lets JS call: await MetaAds.addTestDevices(["HASHED_ID_1", "HASHED_ID_2"])
58
- //
59
- // Accepts a list so a single call can register every test device hash at once; each
60
- // hash is normally copied from Logcat the first time a real ad request is made on that
61
- // device (see MetaAdsInitializer's warm-up call in OnCreate above, or any loadAd()).
62
- Function("addTestDevices") { deviceHashes: List<String> ->
63
- Log.d(MetaAdsInitializer.TAG, "[Module] addTestDevices(${deviceHashes.size} device(s))")
64
- AdSettings.addTestDevices(deviceHashes)
65
- }
66
+ // Lets JS call: await MetaAds.addTestDevices(["HASHED_ID_1", "HASHED_ID_2"])
67
+ //
68
+ // Accepts a list so a single call can register every test device hash at once; each
69
+ // hash is normally copied from Logcat the first time a real ad request is made on that
70
+ // device (see MetaAdsInitializer's warm-up call in OnCreate above, or any loadAd()).
71
+ Function("addTestDevices") { deviceHashes: List<String> ->
72
+ Log.d(MetaAdsInitializer.TAG, "[Module] addTestDevices(${deviceHashes.size} device(s))")
73
+ AdSettings.addTestDevices(deviceHashes)
74
+ }
66
75
 
67
- // Lets JS call: await MetaAds.clearTestDevices()
68
- //
69
- // Mirrors Meta's documented "before shipping" cleanup step
70
- // (AdSettings.setTestMode(false); AdSettings.clearTestDevices();). Exposed separately
71
- // from setTestMode(false) so callers can clear registered device hashes independently.
72
- Function("clearTestDevices") {
73
- Log.d(MetaAdsInitializer.TAG, "[Module] clearTestDevices()")
74
- AdSettings.clearTestDevices()
75
- }
76
+ // Lets JS call: await MetaAds.clearTestDevices()
77
+ //
78
+ // Mirrors Meta's documented "before shipping" cleanup step
79
+ // (AdSettings.setTestMode(false); AdSettings.clearTestDevices();). Exposed separately
80
+ // from setTestMode(false) so callers can clear registered device hashes independently.
81
+ Function("clearTestDevices") {
82
+ Log.d(MetaAdsInitializer.TAG, "[Module] clearTestDevices()")
83
+ AdSettings.clearTestDevices()
84
+ }
76
85
 
77
- // Banner ad native view, exposed to JS as <ExpoAdsMetaBannerView>.
78
- View(MetaBannerAdView::class) {
79
- Events("onAdLoaded", "onAdFailedToLoad", "onAdClicked", "onLoggingImpression")
86
+ AsyncFunction("interstitialLoad") { requestId: Int, unitId: String ->
87
+ val emit: InterstitialEmitter = { body ->
88
+ try {
89
+ sendEvent("onInterstitialEvent", body)
90
+ } catch (
91
+ e: Exception,
92
+ ) {
93
+ Log.w(MetaAdsInitializer.TAG, "[Interstitial] sendEvent failed: ${e.message}")
94
+ }
95
+ }
96
+ val ctx = appContext.currentActivity ?: appContext.reactContext
97
+ if (ctx == null) {
98
+ emit(
99
+ mapOf(
100
+ "requestId" to requestId,
101
+ "type" to "error",
102
+ "code" to "ERR_NO_CONTEXT",
103
+ "message" to "No Activity/React context available",
104
+ ),
105
+ )
106
+ return@AsyncFunction
107
+ }
108
+ MetaInterstitialManager.runOnMain {
109
+ MetaInterstitialManager.load(ctx, requestId, unitId, emit)
110
+ }
111
+ }
112
+
113
+ AsyncFunction("interstitialShow") { requestId: Int, promise: Promise ->
114
+ MetaInterstitialManager.runOnMain {
115
+ val failure = MetaInterstitialManager.show(requestId)
116
+ if (failure == null) {
117
+ promise.resolve(null)
118
+ } else {
119
+ promise.reject(failure.first, failure.second, null)
120
+ }
121
+ }
122
+ }
80
123
 
81
- Prop("unitId") { view: MetaBannerAdView, value: String? ->
82
- view.setUnitId(value)
124
+ Function("interstitialDestroy") { requestId: Int ->
125
+ MetaInterstitialManager.runOnMain { MetaInterstitialManager.destroy(requestId) }
83
126
  }
84
-
85
- Prop("size") { view: MetaBannerAdView, value: MetaBannerAdSize ->
86
- view.setSize(value)
127
+
128
+ AsyncFunction("rewardedLoad") { requestId: Int, unitId: String, userId: String?, currency: String? ->
129
+ val emit: RewardedEmitter = { body ->
130
+ try {
131
+ sendEvent("onRewardedEvent", body)
132
+ } catch (e: Exception) {
133
+ Log.w(MetaAdsInitializer.TAG, "[Rewarded] sendEvent failed: ${e.message}")
134
+ }
135
+ }
136
+ val ctx = appContext.currentActivity ?: appContext.reactContext
137
+ if (ctx == null) {
138
+ emit(
139
+ mapOf(
140
+ "requestId" to requestId,
141
+ "type" to "error",
142
+ "code" to "ERR_NO_CONTEXT",
143
+ "message" to "No Activity/React context available",
144
+ ),
145
+ )
146
+ return@AsyncFunction
147
+ }
148
+ MetaRewardedManager.runOnMain {
149
+ MetaRewardedManager.load(ctx, requestId, unitId, userId, currency, emit)
150
+ }
151
+ }
152
+
153
+ AsyncFunction("rewardedShow") { requestId: Int, promise: Promise ->
154
+ val emit: RewardedEmitter = { body ->
155
+ try {
156
+ sendEvent("onRewardedEvent", body)
157
+ } catch (e: Exception) {
158
+ Log.w(MetaAdsInitializer.TAG, "[Rewarded] sendEvent failed: ${e.message}")
159
+ }
160
+ }
161
+ MetaRewardedManager.runOnMain {
162
+ val failure = MetaRewardedManager.show(requestId, emit)
163
+ if (failure == null) promise.resolve(null) else promise.reject(failure.first, failure.second, null)
164
+ }
87
165
  }
88
166
 
89
- // Fires after unitId/size are applied for this commit, mirroring the
90
- // onAfterUpdateTransaction "props changed -> (re)request" pattern used by the reference
91
- // Google Mobile Ads ViewManager, but expressed as an Expo Modules view lifecycle hook.
92
- OnViewDidUpdateProps { view: MetaBannerAdView ->
93
- view.requestAd()
167
+ AsyncFunction("rewardedIsValid") { requestId: Int, promise: Promise ->
168
+ MetaRewardedManager.runOnMain { promise.resolve(MetaRewardedManager.isValid(requestId)) }
169
+ }
170
+
171
+ Function("rewardedDestroy") { requestId: Int ->
172
+ MetaRewardedManager.runOnMain { MetaRewardedManager.destroy(requestId) }
173
+ }
174
+
175
+ AsyncFunction("rewardedInterstitialLoad") { requestId: Int, unitId: String, userId: String?, currency: String? ->
176
+ val emit: RewardedInterstitialEmitter = { body ->
177
+ try {
178
+ sendEvent("onRewardedInterstitialEvent", body)
179
+ } catch (
180
+ e: Exception,
181
+ ) {
182
+ Log.w(MetaAdsInitializer.TAG, "[RewardedInterstitial] sendEvent failed: ${e.message}")
183
+ }
184
+ }
185
+ val ctx = appContext.currentActivity ?: appContext.reactContext
186
+ if (ctx == null) {
187
+ emit(
188
+ mapOf(
189
+ "requestId" to requestId,
190
+ "type" to "error",
191
+ "code" to "ERR_NO_CONTEXT",
192
+ "message" to "No Activity/React context available",
193
+ ),
194
+ )
195
+ return@AsyncFunction
196
+ }
197
+ MetaRewardedInterstitialManager.runOnMain {
198
+ MetaRewardedInterstitialManager.load(ctx, requestId, unitId, userId, currency, emit)
199
+ }
200
+ }
201
+
202
+ AsyncFunction("rewardedInterstitialShow") { requestId: Int, promise: Promise ->
203
+ val emit: RewardedInterstitialEmitter = { body ->
204
+ try {
205
+ sendEvent("onRewardedInterstitialEvent", body)
206
+ } catch (
207
+ e: Exception,
208
+ ) {
209
+ Log.w(MetaAdsInitializer.TAG, "[RewardedInterstitial] sendEvent failed: ${e.message}")
210
+ }
211
+ }
212
+ MetaRewardedInterstitialManager.runOnMain {
213
+ val failure = MetaRewardedInterstitialManager.show(requestId, emit)
214
+ if (failure == null) promise.resolve(null) else promise.reject(failure.first, failure.second, null)
215
+ }
216
+ }
217
+
218
+ AsyncFunction("rewardedInterstitialIsValid") { requestId: Int, promise: Promise ->
219
+ MetaRewardedInterstitialManager.runOnMain { promise.resolve(MetaRewardedInterstitialManager.isValid(requestId)) }
220
+ }
221
+
222
+ Function("rewardedInterstitialDestroy") { requestId: Int ->
223
+ MetaRewardedInterstitialManager.runOnMain { MetaRewardedInterstitialManager.destroy(requestId) }
224
+ }
225
+
226
+ // Banner ad native view, exposed to JS as <MetaBannerAdView>.
227
+ View(MetaBannerAdView::class) {
228
+ Events("onAdLoaded", "onAdFailedToLoad", "onAdClicked", "onLoggingImpression")
229
+
230
+ Prop("unitId") { view: MetaBannerAdView, value: String? ->
231
+ view.setUnitId(value)
232
+ }
233
+
234
+ Prop("size") { view: MetaBannerAdView, value: MetaBannerAdSize ->
235
+ view.setSize(value)
236
+ }
237
+
238
+ // Fires after unitId/size are applied for this commit, mirroring the
239
+ // onAfterUpdateTransaction "props changed -> (re)request" pattern used by the reference
240
+ // Google Mobile Ads ViewManager, but expressed as an Expo Modules view lifecycle hook.
241
+ OnViewDidUpdateProps { view: MetaBannerAdView -> view.requestAd() }
242
+
243
+ OnViewDestroys { view: MetaBannerAdView -> view.cleanup() }
244
+ }
245
+
246
+ AsyncFunction("nativeAdLoad") { unitId: String, promise: Promise ->
247
+ val ctx = appContext.currentActivity ?: appContext.reactContext
248
+ if (ctx == null) {
249
+ promise.reject("ERR_NO_CONTEXT", "No Activity/React context available", null)
250
+ return@AsyncFunction
251
+ }
252
+ MetaNativeAdManager.runOnMain {
253
+ MetaNativeAdManager.load(
254
+ ctx,
255
+ unitId,
256
+ emit = { body ->
257
+ try {
258
+ sendEvent("onNativeAdEvent", body)
259
+ } catch (
260
+ e: Exception,
261
+ ) {
262
+ Log.w(MetaAdsInitializer.TAG, "[Native] sendEvent failed: ${e.message}")
263
+ }
264
+ },
265
+ onLoaded = { promise.resolve(it) },
266
+ onFailed = { code, msg -> promise.reject(code, msg, null) },
267
+ )
268
+ }
269
+ }
270
+
271
+ Function("nativeAdDestroy") { adId: String ->
272
+ MetaNativeAdManager.runOnMain { MetaNativeAdManager.destroy(adId) }
273
+ }
274
+
275
+ View(MetaNativeAdView::class) {
276
+ Prop("adId") { view: MetaNativeAdView, value: String? -> view.setAdId(value) }
277
+
278
+ AsyncFunction("registerAsset") { view: MetaNativeAdView, assetType: String, reactTag: Int ->
279
+ view.registerAsset(assetType, reactTag)
280
+ }
281
+
282
+ GroupView<MetaNativeAdView> {
283
+ AddChildView { parent: MetaNativeAdView, child: android.view.View, index: Int ->
284
+ parent.addChild(child, index)
285
+ }
286
+ GetChildCount { parent -> parent.childCount() }
287
+ GetChildViewAt { parent, index -> parent.childAt(index) }
288
+ RemoveChildViewAt { parent, index -> parent.removeChildAt(index) }
289
+ }
290
+
291
+ OnViewDestroys { view: MetaNativeAdView -> view.cleanup() }
292
+ }
293
+ View(MetaNativeMediaView::class) {}
294
+ View(MetaNativeIconView::class) {}
295
+ View(MetaNativeAdOptionsView::class) {
296
+ Prop("orientation") { view: MetaNativeAdOptionsView, v: String? -> view.setOrientation(v) }
297
+ Prop("iconSize") { view: MetaNativeAdOptionsView, v: Int? -> view.setIconSize(v) }
298
+ Prop("iconColor") { view: MetaNativeAdOptionsView, v: Int? -> view.setIconColor(v) }
299
+ OnViewDidUpdateProps { view: MetaNativeAdOptionsView -> view.rebuild() }
94
300
  }
95
301
  }
96
- }
97
- }
302
+ }
@@ -18,88 +18,101 @@ class MetaBannerAdView(context: Context, appContext: AppContext) : ExpoView(cont
18
18
  private var adView: AdView? = null
19
19
  private var unitId: String? = null
20
20
  private var size: MetaBannerAdSize = MetaBannerAdSize.BANNER_HEIGHT_50
21
+ private var loadedUnitId: String? = null
22
+ private var loadedSize: MetaBannerAdSize? = null
23
+ private var destroyed = false
21
24
 
22
25
  private val onAdLoaded by EventDispatcher()
23
26
  private val onAdFailedToLoad by EventDispatcher()
24
27
  private val onAdClicked by EventDispatcher()
25
28
  private val onLoggingImpression by EventDispatcher()
26
29
 
27
- fun setUnitId(value: String?) {
28
- unitId = value
29
- }
30
+ fun setUnitId(value: String?) { unitId = value }
31
+ fun setSize(value: MetaBannerAdSize) { size = value }
30
32
 
31
- fun setSize(value: MetaBannerAdSize) {
32
- size = value
33
+ // Swallows dispatches to a dead React context instead of crashing
34
+ private fun safeDispatch(block: () -> Unit) {
35
+ if (destroyed) return
36
+ try {
37
+ block()
38
+ } catch (e: Exception) {
39
+ Log.w(TAG, "[Banner] event dispatch failed: ${e.message}")
40
+ }
33
41
  }
34
42
 
35
- /**
36
- * Called from `OnViewDidUpdateProps` in MetaAdsModule after unitId/size are applied for a
37
- * commit (mirroring the "props changed -> (re)request" pattern from the reference Google
38
- * Mobile Ads ViewManager's onAfterUpdateTransaction). Runs the two required guards, in
39
- * order, before ever touching the SDK:
40
- * 1. Ad unit must be configured — otherwise don't build a request at all.
41
- * 2. SDK must already report READY — otherwise tell the caller to initialize first.
42
- * Neither failure throws; both are surfaced to JS as onAdFailedToLoad so callers can
43
- * handle them the same way they'd handle any other load failure.
44
- */
45
43
  fun requestAd() {
44
+ if (destroyed) return
46
45
  val currentUnitId = unitId
47
46
 
48
47
  if (currentUnitId.isNullOrBlank()) {
49
48
  val message = "Meta banner ad request skipped: no adUnitId was provided. Set the " +
50
49
  "`unitId` prop on <MetaBannerAd> to a valid Audience Network placement ID."
51
50
  Log.e(TAG, "[Banner] $message")
52
- onAdFailedToLoad(mapOf("code" to "ERR_NO_AD_UNIT", "message" to message))
51
+ safeDispatch { onAdFailedToLoad(mapOf("code" to "ERR_NO_AD_UNIT", "message" to message)) }
53
52
  return
54
53
  }
55
54
 
56
55
  if (!MetaAdsInitializer.isReady()) {
57
56
  val message = "Meta banner ad request skipped: Audience Network SDK is not initialized " +
58
- "yet. Call ExpoAdsMeta.initialize() and wait for it to resolve before requesting a " +
57
+ "yet. Call MetaAds.initialize() and wait for it to resolve before requesting a " +
59
58
  "banner ad."
60
59
  Log.e(TAG, "[Banner] $message")
61
- onAdFailedToLoad(mapOf("code" to "ERR_NOT_INITIALIZED", "message" to message))
60
+ safeDispatch { onAdFailedToLoad(mapOf("code" to "ERR_NOT_INITIALIZED", "message" to message)) }
62
61
  return
63
62
  }
64
63
 
64
+ // Skip redundant reloads when nothing changed
65
+ if (adView != null && loadedUnitId == currentUnitId && loadedSize == size) return
66
+
65
67
  loadAd(currentUnitId, size)
66
68
  }
67
69
 
68
70
  private fun loadAd(currentUnitId: String, currentSize: MetaBannerAdSize) {
69
- // Tear down any previous ad view before creating a new one (unit or size may have changed).
70
71
  destroyAdView()
72
+ loadedUnitId = currentUnitId
73
+ loadedSize = currentSize
71
74
 
72
75
  val newAdView = AdView(context, currentUnitId, currentSize.toFacebookAdSize())
73
76
  adView = newAdView
74
77
 
75
78
  val adListener = object : AdListener {
76
79
  override fun onError(ad: Ad, adError: AdError) {
80
+ if (adView !== newAdView) return // stale callback from a replaced ad
77
81
  Log.e(TAG, "[Banner] onError: ${adError.errorCode} ${adError.errorMessage}")
78
- onAdFailedToLoad(
79
- mapOf(
80
- "code" to adError.errorCode.toString(),
81
- "message" to adError.errorMessage,
82
+ safeDispatch {
83
+ onAdFailedToLoad(
84
+ mapOf(
85
+ "code" to adError.errorCode.toString(),
86
+ "message" to adError.errorMessage,
87
+ )
82
88
  )
83
- )
89
+ }
84
90
  }
85
91
 
86
92
  override fun onAdLoaded(ad: Ad) {
93
+ if (adView !== newAdView) return
87
94
  Log.d(TAG, "[Banner] onAdLoaded")
88
- onAdLoaded(
89
- mapOf(
90
- "width" to currentSize.toFacebookAdSize().width,
91
- "height" to currentSize.toFacebookAdSize().height,
95
+ safeDispatch {
96
+ onAdLoaded(
97
+ mapOf(
98
+ "width" to currentSize.toFacebookAdSize().width,
99
+ "height" to currentSize.toFacebookAdSize().height,
100
+ )
92
101
  )
93
- )
102
+ }
103
+ // Force a layout pass so RN shows the natively-added child
104
+ post {
105
+ measure(
106
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
107
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
108
+ )
109
+ layout(left, top, right, bottom)
110
+ }
94
111
  }
95
112
 
96
- override fun onAdClicked(ad: Ad) {
97
- onAdClicked(emptyMap())
98
- }
113
+ override fun onAdClicked(ad: Ad) = safeDispatch { onAdClicked(emptyMap()) }
99
114
 
100
- override fun onLoggingImpression(ad: Ad) {
101
- onLoggingImpression(emptyMap())
102
- }
115
+ override fun onLoggingImpression(ad: Ad) = safeDispatch { onLoggingImpression(emptyMap()) }
103
116
  }
104
117
 
105
118
  addView(
@@ -107,21 +120,48 @@ class MetaBannerAdView(context: Context, appContext: AppContext) : ExpoView(cont
107
120
  ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
108
121
  )
109
122
 
110
- // AdView has no setAdListener(...) setter — the listener is attached through the
111
- // load-config builder and only takes effect for this particular loadAd() call.
112
123
  newAdView.loadAd(newAdView.buildLoadAdConfig().withAdListener(adListener).build())
113
124
  }
114
125
 
115
126
  private fun destroyAdView() {
116
127
  adView?.let {
117
- removeView(it)
118
- it.destroy()
128
+ try {
129
+ removeView(it)
130
+ it.destroy()
131
+ } catch (e: Exception) {
132
+ Log.w(TAG, "[Banner] destroy failed: ${e.message}")
133
+ }
119
134
  }
120
135
  adView = null
136
+ loadedUnitId = null
137
+ loadedSize = null
138
+ }
139
+
140
+ // Called from OnViewDestroys in MetaAdsModule when React drops this view
141
+ fun cleanup() {
142
+ destroyed = true
143
+ destroyAdView()
144
+ }
145
+
146
+ override fun onAttachedToWindow() {
147
+ super.onAttachedToWindow()
148
+ destroyed = false
121
149
  }
122
150
 
123
151
  override fun onDetachedFromWindow() {
124
152
  destroyAdView()
125
153
  super.onDetachedFromWindow()
126
154
  }
155
+
156
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
157
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
158
+ adView?.measure(
159
+ MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
160
+ MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.AT_MOST)
161
+ )
162
+ }
163
+
164
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
165
+ adView?.let { it.layout(0, 0, it.measuredWidth, it.measuredHeight) }
166
+ }
127
167
  }