use std::collections::HashMap;
use std::ffi::CString;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use crate::ffi::read_cstr;
#[cfg(target_os = "ios")]
#[path = "backend_ios.rs"]
mod backend;
#[cfg(not(target_os = "ios"))]
#[path = "backend_fake.rs"]
mod backend;
pub const TEST_APP_ID: &str = "ca-app-pub-3940256099942544~1458002511";
pub fn test_unit_id(format: AdFormat) -> &'static str {
match format {
AdFormat::Banner => "ca-app-pub-3940256099942544/2934735716",
AdFormat::Interstitial => "ca-app-pub-3940256099942544/4411468910",
AdFormat::Rewarded => "ca-app-pub-3940256099942544/1712485313",
AdFormat::RewardedInterstitial => "ca-app-pub-3940256099942544/6978759866",
AdFormat::AppOpen => "ca-app-pub-3940256099942544/5575463023",
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
pub enum AdFormat {
Banner,
Interstitial,
Rewarded,
RewardedInterstitial,
AppOpen,
}
impl AdFormat {
pub const ALL: [AdFormat; 5] = [
AdFormat::Banner,
AdFormat::Interstitial,
AdFormat::Rewarded,
AdFormat::RewardedInterstitial,
AdFormat::AppOpen,
];
pub fn is_full_screen(self) -> bool {
!matches!(self, AdFormat::Banner)
}
pub fn as_i32(self) -> i32 {
match self {
AdFormat::Banner => 0,
AdFormat::Interstitial => 1,
AdFormat::Rewarded => 2,
AdFormat::RewardedInterstitial => 3,
AdFormat::AppOpen => 4,
}
}
pub fn from_i32(v: i32) -> Option<AdFormat> {
Some(match v {
0 => AdFormat::Banner,
1 => AdFormat::Interstitial,
2 => AdFormat::Rewarded,
3 => AdFormat::RewardedInterstitial,
4 => AdFormat::AppOpen,
_ => return None,
})
}
#[cfg_attr(target_os = "ios", allow(dead_code))]
fn from_token(token: &str) -> Option<AdFormat> {
Some(match token.trim().to_ascii_lowercase().as_str() {
"banner" => AdFormat::Banner,
"interstitial" => AdFormat::Interstitial,
"rewarded" => AdFormat::Rewarded,
"rewardedinterstitial" | "rewarded_interstitial" => AdFormat::RewardedInterstitial,
"appopen" | "app_open" => AdFormat::AppOpen,
_ => return None,
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum AdLoadState {
#[default]
Idle,
Loading,
Loaded,
Failed,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum BannerPosition {
Top,
#[default]
Bottom,
}
impl BannerPosition {
fn as_i32(self) -> i32 {
match self {
BannerPosition::Top => 0,
BannerPosition::Bottom => 1,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ConsentStatus {
#[default]
Unknown,
Required,
NotRequired,
Obtained,
}
impl ConsentStatus {
fn from_i32(v: i32) -> ConsentStatus {
match v {
1 => ConsentStatus::Required,
2 => ConsentStatus::NotRequired,
3 => ConsentStatus::Obtained,
_ => ConsentStatus::Unknown,
}
}
#[deprecated(since = "0.3.3", note = "use AdmobState::can_request_ads")]
pub fn can_request_ads(self) -> bool {
matches!(self, Self::NotRequired | Self::Obtained)
}
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum PrivacyOptionsRequirement {
#[default]
Unknown,
Required,
NotRequired,
}
impl PrivacyOptionsRequirement {
fn from_i32(v: i32) -> Self {
match v {
1 => Self::Required,
2 => Self::NotRequired,
_ => Self::Unknown,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum UmpDebugGeography {
Eea,
RegulatedUsState,
Other,
}
impl UmpDebugGeography {
fn as_i32(self) -> i32 {
match self {
Self::Eea => 1,
Self::RegulatedUsState => 3,
Self::Other => 4,
}
}
}
#[derive(Resource, Clone, Default)]
pub struct AdmobConfig {
pub unit_ids: HashMap<AdFormat, String>,
pub test_device_ids: Vec<String>,
pub use_test_ads: bool,
}
impl AdmobConfig {
pub fn test_ads() -> Self {
Self {
use_test_ads: true,
..Default::default()
}
}
pub fn with_unit(mut self, format: AdFormat, unit_id: impl Into<String>) -> Self {
self.unit_ids.insert(format, unit_id.into());
self
}
pub fn resolve_unit(&self, format: AdFormat) -> String {
if self.use_test_ads {
return test_unit_id(format).to_string();
}
match self.unit_ids.get(&format) {
Some(id) if !id.is_empty() => id.clone(),
_ => test_unit_id(format).to_string(),
}
}
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct UmpTestConfig {
pub geography: Option<UmpDebugGeography>,
pub reset_consent_on_start: bool,
}
impl UmpTestConfig {
fn wire_values(self, use_test_ads: bool) -> (i32, i32) {
if use_test_ads {
(
self.geography.map(UmpDebugGeography::as_i32).unwrap_or(0),
self.reset_consent_on_start as i32,
)
} else {
(0, 0)
}
}
}
#[derive(Resource, Default)]
pub struct AdInventory {
states: HashMap<AdFormat, AdLoadState>,
}
impl AdInventory {
pub fn state(&self, format: AdFormat) -> AdLoadState {
self.states.get(&format).copied().unwrap_or_default()
}
pub fn is_loaded(&self, format: AdFormat) -> bool {
self.state(format) == AdLoadState::Loaded
}
pub fn is_loading(&self, format: AdFormat) -> bool {
self.state(format) == AdLoadState::Loading
}
fn set(&mut self, format: AdFormat, state: AdLoadState) {
self.states.insert(format, state);
}
}
#[derive(Resource, Default)]
pub struct AdmobState {
pub initialized: bool,
pub consent: ConsentStatus,
pub can_request_ads: bool,
pub banner_visible: bool,
}
#[derive(Message, Clone, Debug)]
pub struct LoadAd(pub AdFormat);
#[derive(Message, Clone, Debug)]
pub struct ShowAd(pub AdFormat);
#[derive(Message, Clone, Debug, Default)]
pub struct ShowBanner {
pub position: BannerPosition,
}
#[derive(Message, Clone, Debug)]
pub struct HideBanner;
#[derive(Message, Clone, Debug)]
pub struct RequestConsent;
#[derive(Message, Clone, Debug)]
pub struct PresentPrivacyOptions;
#[derive(Message, Clone, Debug)]
pub struct AdLoaded(pub AdFormat);
#[derive(Message, Clone, Debug)]
pub struct AdLoadFailed {
pub format: AdFormat,
pub error: String,
}
#[derive(Message, Clone, Debug)]
pub struct AdShown(pub AdFormat);
#[derive(Message, Clone, Debug)]
pub struct AdDismissed(pub AdFormat);
#[derive(Message, Clone, Debug)]
pub struct AdShowFailed {
pub format: AdFormat,
pub error: String,
}
#[derive(Message, Clone, Debug)]
pub struct RewardEarned {
pub format: AdFormat,
pub amount: i64,
pub reward_type: String,
}
#[derive(Message, Clone, Debug)]
pub struct AdClicked(pub AdFormat);
#[derive(Message, Clone, Debug)]
pub struct ConsentUpdated(pub ConsentStatus);
#[derive(Message, Clone, Debug)]
pub struct ConsentInfoUpdateFailed {
pub error: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(crate) struct AdEvent {
pub format: i32,
pub kind: String,
#[serde(default)]
pub error: String,
#[serde(default)]
pub reward_amount: i64,
#[serde(default)]
pub reward_type: String,
}
fn init(config: &AdmobConfig, ump_test: UmpTestConfig) {
let Ok(joined) = CString::new(config.test_device_ids.join(",")) else {
return;
};
let (debug_geography, reset_consent) = ump_test.wire_values(config.use_test_ads);
unsafe {
backend::admob_init_with_ump_test(
joined.as_ptr(),
config.use_test_ads as i32,
debug_geography,
reset_consent,
)
};
}
fn load(format: AdFormat, unit_id: &str) {
let Ok(unit) = CString::new(unit_id) else {
return;
};
unsafe { backend::admob_load(format.as_i32(), unit.as_ptr()) };
}
fn show(format: AdFormat) {
unsafe { backend::admob_show(format.as_i32()) };
}
fn banner_show(unit_id: &str, position: BannerPosition) {
let Ok(unit) = CString::new(unit_id) else {
return;
};
unsafe { backend::admob_banner_show(unit.as_ptr(), position.as_i32()) };
}
fn banner_hide() {
unsafe { backend::admob_banner_hide() };
}
fn request_consent() {
unsafe { backend::admob_request_consent() };
}
fn present_privacy_options() {
unsafe { backend::admob_present_privacy_options() };
}
fn consent_status() -> ConsentStatus {
ConsentStatus::from_i32(unsafe { backend::admob_consent_status() })
}
fn can_request_ads() -> bool {
unsafe { backend::admob_can_request_ads() != 0 }
}
fn privacy_options_requirement() -> PrivacyOptionsRequirement {
PrivacyOptionsRequirement::from_i32(unsafe {
backend::admob_privacy_options_requirement_status()
})
}
fn drain_events() -> Vec<AdEvent> {
let json = unsafe { read_cstr(backend::admob_drain_events()) };
serde_json::from_str(&json).unwrap_or_default()
}
#[derive(Resource, Default)]
struct AdsPoll {
inited: bool,
consent: ConsentStatus,
}
pub struct AdsPlugin;
impl Plugin for AdsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<AdInventory>()
.init_resource::<AdmobState>()
.init_resource::<PrivacyOptionsRequirement>()
.init_resource::<UmpTestConfig>()
.init_resource::<AdsPoll>()
.add_message::<LoadAd>()
.add_message::<ShowAd>()
.add_message::<ShowBanner>()
.add_message::<HideBanner>()
.add_message::<RequestConsent>()
.add_message::<PresentPrivacyOptions>()
.add_message::<AdLoaded>()
.add_message::<AdLoadFailed>()
.add_message::<AdShown>()
.add_message::<AdDismissed>()
.add_message::<AdShowFailed>()
.add_message::<RewardEarned>()
.add_message::<AdClicked>()
.add_message::<ConsentUpdated>()
.add_message::<ConsentInfoUpdateFailed>()
.add_systems(Update, (init_once, pump_requests, poll_backend).chain());
}
}
fn init_once(
config: Option<Res<AdmobConfig>>,
ump_test: Res<UmpTestConfig>,
mut poll: ResMut<AdsPoll>,
mut state: ResMut<AdmobState>,
) {
if poll.inited {
return;
}
if let Some(config) = config {
init(&config, *ump_test);
poll.inited = true;
state.initialized = true;
}
}
#[allow(clippy::too_many_arguments)]
fn pump_requests(
poll: Res<AdsPoll>,
config: Option<Res<AdmobConfig>>,
mut inventory: ResMut<AdInventory>,
mut state: ResMut<AdmobState>,
mut loads: MessageReader<LoadAd>,
mut shows: MessageReader<ShowAd>,
mut banner_shows: MessageReader<ShowBanner>,
mut banner_hides: MessageReader<HideBanner>,
mut consents: MessageReader<RequestConsent>,
mut privacy_options: MessageReader<PresentPrivacyOptions>,
) {
if !poll.inited {
return;
}
let resolve = |format: AdFormat| {
config
.as_ref()
.map(|c| c.resolve_unit(format))
.unwrap_or_else(|| test_unit_id(format).to_string())
};
for LoadAd(format) in loads.read() {
if format.is_full_screen() {
inventory.set(*format, AdLoadState::Loading);
load(*format, &resolve(*format));
}
}
for ShowAd(format) in shows.read() {
show(*format);
}
for ShowBanner { position } in banner_shows.read() {
banner_show(&resolve(AdFormat::Banner), *position);
state.banner_visible = true;
}
for _ in banner_hides.read() {
banner_hide();
state.banner_visible = false;
}
for _ in consents.read() {
request_consent();
}
for _ in privacy_options.read() {
present_privacy_options();
}
}
#[allow(clippy::too_many_arguments)]
fn poll_backend(
mut poll: ResMut<AdsPoll>,
mut inventory: ResMut<AdInventory>,
mut state: ResMut<AdmobState>,
mut privacy_options_state: ResMut<PrivacyOptionsRequirement>,
mut loaded: MessageWriter<AdLoaded>,
mut load_failed: MessageWriter<AdLoadFailed>,
mut shown: MessageWriter<AdShown>,
mut dismissed: MessageWriter<AdDismissed>,
mut show_failed: MessageWriter<AdShowFailed>,
mut reward: MessageWriter<RewardEarned>,
mut clicked: MessageWriter<AdClicked>,
mut consent_updated: MessageWriter<ConsentUpdated>,
mut consent_update_failed: MessageWriter<ConsentInfoUpdateFailed>,
) {
if !poll.inited {
return;
}
let consent = consent_status();
if consent != poll.consent {
poll.consent = consent;
state.consent = consent;
consent_updated.write(ConsentUpdated(consent));
}
let ready = can_request_ads();
if state.can_request_ads != ready {
state.can_request_ads = ready;
}
let privacy_options = privacy_options_requirement();
if privacy_options != *privacy_options_state {
*privacy_options_state = privacy_options;
}
for ev in drain_events() {
if ev.kind == "consent_update_failed" {
consent_update_failed.write(ConsentInfoUpdateFailed { error: ev.error });
continue;
}
let Some(format) = AdFormat::from_i32(ev.format) else {
continue;
};
match ev.kind.as_str() {
"loaded" => {
inventory.set(format, AdLoadState::Loaded);
loaded.write(AdLoaded(format));
}
"load_failed" => {
inventory.set(format, AdLoadState::Failed);
load_failed.write(AdLoadFailed {
format,
error: ev.error,
});
}
"shown" => {
if format.is_full_screen() {
inventory.set(format, AdLoadState::Idle);
}
shown.write(AdShown(format));
}
"dismissed" => {
dismissed.write(AdDismissed(format));
}
"show_failed" => {
if format.is_full_screen() {
inventory.set(format, AdLoadState::Idle);
}
show_failed.write(AdShowFailed {
format,
error: ev.error,
});
}
"reward" => {
reward.write(RewardEarned {
format,
amount: ev.reward_amount,
reward_type: ev.reward_type,
});
}
"clicked" => {
clicked.write(AdClicked(format));
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn load_interstitial_once(mut loads: MessageWriter<LoadAd>, mut fired: Local<bool>) {
if !*fired {
*fired = true;
loads.write(LoadAd(AdFormat::Interstitial));
}
}
fn show_when_loaded(
inventory: Res<AdInventory>,
mut shows: MessageWriter<ShowAd>,
mut fired: Local<bool>,
) {
if !*fired && inventory.is_loaded(AdFormat::Interstitial) {
*fired = true;
shows.write(ShowAd(AdFormat::Interstitial));
}
}
fn build_app() -> App {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins(AdsPlugin);
app
}
static FAKE_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn guarded() -> std::sync::MutexGuard<'static, ()> {
let g = FAKE_GUARD.lock().unwrap_or_else(|p| p.into_inner());
backend::reset();
for key in [
"BEVY_ADMOB_FAKE_NO_FILL",
"BEVY_ADMOB_FAKE_SHOW_FAIL",
"BEVY_ADMOB_FAKE_REWARD_AMOUNT",
"BEVY_ADMOB_FAKE_REWARD_TYPE",
"BEVY_ADMOB_FAKE_CONSENT",
"BEVY_ADMOB_FAKE_CAN_REQUEST_ADS",
"BEVY_ADMOB_FAKE_CONSENT_UPDATE",
"BEVY_ADMOB_FAKE_PRIVACY_OPTIONS",
] {
unsafe { std::env::remove_var(key) };
}
g
}
#[test]
fn format_i32_round_trips() {
for f in AdFormat::ALL {
assert_eq!(AdFormat::from_i32(f.as_i32()), Some(f));
}
assert_eq!(AdFormat::from_i32(99), None);
}
#[test]
fn resolve_unit_prefers_config_then_falls_back_to_test() {
let cfg = AdmobConfig::default().with_unit(AdFormat::Interstitial, "ca-app-pub-x/y");
assert_eq!(cfg.resolve_unit(AdFormat::Interstitial), "ca-app-pub-x/y");
assert_eq!(
cfg.resolve_unit(AdFormat::Rewarded),
test_unit_id(AdFormat::Rewarded)
);
let test_cfg = AdmobConfig::test_ads().with_unit(AdFormat::Interstitial, "ca-app-pub-x/y");
assert_eq!(
test_cfg.resolve_unit(AdFormat::Interstitial),
test_unit_id(AdFormat::Interstitial)
);
}
#[test]
fn consent_debug_overrides_are_disabled_for_production_ads() {
let _guard = guarded();
let mut app = build_app();
app.insert_resource(AdmobConfig::default());
app.insert_resource(UmpTestConfig {
geography: Some(UmpDebugGeography::Eea),
reset_consent_on_start: true,
});
app.update();
assert_eq!(backend::ump_test_config(), (0, false));
}
#[derive(Resource, Default)]
struct Recorder {
loaded: u32,
dismissed: u32,
rewards: Vec<RewardEarned>,
}
fn record(
mut rec: ResMut<Recorder>,
mut loaded: MessageReader<AdLoaded>,
mut dismissed: MessageReader<AdDismissed>,
mut rewards: MessageReader<RewardEarned>,
) {
rec.loaded += loaded.read().count() as u32;
rec.dismissed += dismissed.read().count() as u32;
for r in rewards.read() {
rec.rewards.push(r.clone());
}
}
fn show_rewarded_once(
inv: Res<AdInventory>,
mut shows: MessageWriter<ShowAd>,
mut fired: Local<bool>,
) {
if !*fired && inv.is_loaded(AdFormat::Rewarded) {
*fired = true;
shows.write(ShowAd(AdFormat::Rewarded));
}
}
#[test]
fn interstitial_load_show_dismiss_flow() {
let _guard = guarded();
let mut app = build_app();
app.init_resource::<Recorder>();
app.insert_resource(AdmobConfig::test_ads());
app.add_systems(Update, (load_interstitial_once, show_when_loaded, record));
for _ in 0..10 {
app.update();
}
let rec = app.world().resource::<Recorder>();
assert_eq!(
rec.loaded, 1,
"interstitial should have reported loaded once"
);
assert_eq!(rec.dismissed, 1, "interstitial should have dismissed once");
assert_eq!(
app.world()
.resource::<AdInventory>()
.state(AdFormat::Interstitial),
AdLoadState::Idle
);
}
#[test]
fn rewarded_grants_reward() {
let _guard = guarded();
unsafe {
std::env::set_var("BEVY_ADMOB_FAKE_REWARD_AMOUNT", "7");
std::env::set_var("BEVY_ADMOB_FAKE_REWARD_TYPE", "gems");
}
let mut app = build_app();
app.init_resource::<Recorder>();
app.insert_resource(AdmobConfig::test_ads());
app.add_systems(
Update,
(
|mut loads: MessageWriter<LoadAd>, mut fired: Local<bool>| {
if !*fired {
*fired = true;
loads.write(LoadAd(AdFormat::Rewarded));
}
},
show_rewarded_once,
record,
),
);
for _ in 0..10 {
app.update();
}
let rec = app.world().resource::<Recorder>();
assert_eq!(
rec.rewards.len(),
1,
"rewarded ad should grant exactly one reward"
);
assert_eq!(rec.rewards[0].amount, 7);
assert_eq!(rec.rewards[0].reward_type, "gems");
assert_eq!(rec.dismissed, 1, "rewarded ad should also dismiss");
}
#[test]
fn no_fill_reports_failure() {
let _guard = guarded();
unsafe { std::env::set_var("BEVY_ADMOB_FAKE_NO_FILL", "appopen") }
let mut app = build_app();
app.insert_resource(AdmobConfig::test_ads());
app.add_systems(
Update,
|mut loads: MessageWriter<LoadAd>, mut fired: Local<bool>| {
if !*fired {
*fired = true;
loads.write(LoadAd(AdFormat::AppOpen));
}
},
);
for _ in 0..6 {
app.update();
}
assert_eq!(
app.world()
.resource::<AdInventory>()
.state(AdFormat::AppOpen),
AdLoadState::Failed
);
}
#[test]
fn consent_required_then_obtained() {
let _guard = guarded();
unsafe { std::env::set_var("BEVY_ADMOB_FAKE_CONSENT", "required") }
let mut app = build_app();
app.insert_resource(AdmobConfig::test_ads());
app.update(); assert_eq!(
app.world().resource::<AdmobState>().consent,
ConsentStatus::Required
);
app.world_mut()
.resource_mut::<Messages<RequestConsent>>()
.write(RequestConsent);
app.update(); app.update();
assert_eq!(
app.world().resource::<AdmobState>().consent,
ConsentStatus::Obtained
);
assert!(app.world().resource::<AdmobState>().can_request_ads);
}
#[test]
fn native_readiness_is_not_inferred_from_coarse_consent_status() {
let _guard = guarded();
unsafe {
std::env::set_var("BEVY_ADMOB_FAKE_CONSENT", "unknown");
std::env::set_var("BEVY_ADMOB_FAKE_CAN_REQUEST_ADS", "true");
}
let mut app = build_app();
app.insert_resource(AdmobConfig::test_ads());
app.update();
let state = app.world().resource::<AdmobState>();
assert_eq!(state.consent, ConsentStatus::Unknown);
assert!(state.can_request_ads);
}
#[derive(Resource, Default)]
struct ConsentFailureRecorder(Vec<String>);
fn record_consent_failures(
mut recorder: ResMut<ConsentFailureRecorder>,
mut failures: MessageReader<ConsentInfoUpdateFailed>,
) {
recorder
.0
.extend(failures.read().map(|failure| failure.error.clone()));
}
#[test]
fn consent_update_failure_is_observable_and_can_recover() {
let _guard = guarded();
unsafe {
std::env::set_var("BEVY_ADMOB_FAKE_CONSENT", "unknown");
std::env::set_var("BEVY_ADMOB_FAKE_CONSENT_UPDATE", "fail_once");
}
let mut app = build_app();
app.init_resource::<ConsentFailureRecorder>()
.insert_resource(AdmobConfig::test_ads())
.add_systems(Update, record_consent_failures);
app.update();
app.update();
assert!(!app.world().resource::<AdmobState>().can_request_ads);
assert_eq!(app.world().resource::<ConsentFailureRecorder>().0.len(), 1);
app.world_mut()
.resource_mut::<Messages<RequestConsent>>()
.write(RequestConsent);
app.update();
app.update();
let state = app.world().resource::<AdmobState>();
assert_eq!(state.consent, ConsentStatus::Obtained);
assert!(state.can_request_ads);
}
#[test]
fn required_privacy_options_are_polled_and_presented_from_a_request() {
let _guard = guarded();
unsafe { std::env::set_var("BEVY_ADMOB_FAKE_PRIVACY_OPTIONS", "required") }
let mut app = build_app();
app.insert_resource(AdmobConfig::test_ads());
app.update();
assert_eq!(
*app.world().resource::<PrivacyOptionsRequirement>(),
PrivacyOptionsRequirement::Required
);
assert_eq!(backend::privacy_options_presentations(), 0);
app.world_mut()
.resource_mut::<Messages<PresentPrivacyOptions>>()
.write(PresentPrivacyOptions);
app.update();
assert_eq!(backend::privacy_options_presentations(), 1);
assert_eq!(
*app.world().resource::<PrivacyOptionsRequirement>(),
PrivacyOptionsRequirement::Required
);
}
#[test]
fn privacy_options_are_not_required_without_a_fake_override() {
let _guard = guarded();
let mut app = build_app();
app.insert_resource(AdmobConfig::test_ads());
app.update();
assert_eq!(
*app.world().resource::<PrivacyOptionsRequirement>(),
PrivacyOptionsRequirement::NotRequired
);
}
#[test]
fn every_debug_geography_and_reset_reach_the_backend_for_test_ads() {
let _guard = guarded();
for (geography, wire) in [
(UmpDebugGeography::Eea, 1),
(UmpDebugGeography::RegulatedUsState, 3),
(UmpDebugGeography::Other, 4),
] {
backend::reset();
let mut app = build_app();
app.insert_resource(AdmobConfig::test_ads());
app.insert_resource(UmpTestConfig {
geography: Some(geography),
reset_consent_on_start: true,
});
app.update();
assert_eq!(backend::ump_test_config(), (wire, true));
}
}
#[test]
fn banner_show_hide_toggles_state() {
let _guard = guarded();
let mut app = build_app();
app.insert_resource(AdmobConfig::test_ads());
app.update(); app.world_mut()
.resource_mut::<Messages<ShowBanner>>()
.write(ShowBanner::default());
app.update();
assert!(app.world().resource::<AdmobState>().banner_visible);
app.world_mut()
.resource_mut::<Messages<HideBanner>>()
.write(HideBanner);
app.update();
assert!(!app.world().resource::<AdmobState>().banner_visible);
}
}