applovin_admob_sdk
Dual-provider ad SDK for Flutter (AdMob + AppLovin MAX). Offline-verified VIP codes, GDPR/COPPA/CCPA consent, safety caps, AdEvent stream, debug overlay.
2.3.3
835 downloads/mo
#622 most downloaded on pub.dev
royt93/FlutterBase2025
What this package is like to depend on
Last release today
23 Aug 2026
Ships fairly regularly
a new release about every 3 weeks
Most releases are documented
notes for 24 of 38 stable releases
Nothing withdrawn
no release was ever pulled
5 months old
38 releases · first in 2026
38 releases in the last 12 months
see the full history below
Release timeline
38 releases · Mar 2026 to Aug 2026Releases
latest 38-
2.3.323 Aug 2026Release notes
Open source →Six more independent QC rounds (7-12) over the whole package, against the seven product requirements. Every claim below is backed by a test verified red against its own reverted fix — nothing else. Full write-ups in
doc/audit/.Security
- A withdrawn consent no longer leaves a personalised load in flight. Only one of the three consent axes was tracked, so withdrawing while a request was already out let that request complete and serve under the old string. Every slot loaded under a superseded consent epoch is now refused at show time and reloaded.
- UMP
obtainedis no longer read as consent to personalised ads. It only means the user answered the form; the actual TCF purposes are now parsed before anything personalised is requested. - A missing UMP platform channel fails closed in release too. It used to fail open, which on a device where the channel was unavailable meant serving ads to an EEA user who was never asked.
- No ad can be drawn over an open consent form. Presenting the privacy options / re-consent form now blocks full-screen ads for as long as the form is up (ref-counted, with a logged 15-minute backstop so a dropped dismiss callback cannot block ads for the rest of the process).
- A plaintext-fallback VIP grant is only trusted when the Keystore is broken. That fallback exists for devices whose secure storage does not work; a fallback entry on a device whose Keystore is healthy has no legitimate way to exist, so it is now clamped instead of accepted outright.
- A revocation list now clamps grants the revoked key already made, and the cached list is applied on every startup, not only when a fresh one is fetched.
- A host re-init no longer forgives an invalid-traffic escalation, and
bypassSafety: true(the splash App Open ad) no longer skips the invalid-traffic pause.
Fixed
- Blank banner/MREC that never recovered. The rate limiter reported a throttled load as an error, which the recovery loop treated as a broken slot, which re-entered the limiter — a grey box for the rest of the session. Display errors and "needs recovery" are now separate states, and the recovery bypass is itself rate-limited.
- A full-screen slot could wedge for the session. A swallowed show on
either provider left the slot in
showingforever; both providers now release it. - AppLovin banner/MREC could stick in
loadingforever (watchdog added) and could resurrect a key disposed mid-preload, leaking the native ad view. - A transient secure-storage read no longer costs a paying customer their VIP. A failed read is now told apart from "no VIP data" and retried inside the session instead of running the whole session as non-VIP.
- VIP writes are strictly ordered process-wide. A discarded manager's in-flight write could land on top of its replacement's and resurrect an entitlement that had just been revoked. Startup stays bounded: the load's drain gives up rather than hanging on a wedged platform write.
- A redeem on a disposed manager no longer burns the customer's one-time key.
- The ad-click latch is spent by the resume that saw it, so returning from an ad click cannot trigger an App Open ad.
- The M6 fallback clamp is anchored to the grant timestamp, so a failed save no longer rolls the clamp forward on every launch.
Fixed — lifecycle & cache-expiry round (audit MINOR m15/m16/m18/m22/m24/m36)
Each item below is backed by a test that was verified red against its own reverted fix, and nothing else.
- A cached ad could read as "fresh" for the rest of the session after a
clock change. The freshness check compared wall-clock
nowagainst the wall-clock load stamp with no lower bound, so a backwards clock change (manual, or an NTP correction) put the stamp in the future, made the computed age negative, and kept the ad inside its validity window forever. Both the reuse-on-load and the refuse-to-show-a-stale-ad guards stopped working. A negative age now counts as stale. - Discarding an expired ad blocked its own replacement. All four AdMob full-screen formats recorded a cache expiry as a load failure, which starts the exponential-backoff cooldown. The refill fires from the very callback the discard invokes, so it landed inside a cooldown window the discard had just created and the slot stayed empty until the next periodic retry — no ad for the next several show attempts. An expiry now just empties the slot; the load path never failed.
canShowInterstitial()/canShowRewardedAd()could reporttruefor an ad that would not be shown. Both only asked whether the slot was ready, so a cached AdMob ad that aged past its 1h content validity while the host was polling still reported showable — and the show call then discarded it. A host gating a button on these got a button that did nothing.- Revenue could be reported for a disposed full-screen ad. Every
google_mobile_adswrapper cleared its full-screen content callback on dispose but left the paid-event listener wired, so a paid event arriving after disposal still emitted revenue through the old event sink. - AppLovin: destroy retries outlived the adapter. The retry that makes
destroyWidgetAdViewsucceed once the native view has finished detaching slept on an untracked timer, so a chain started by the last widget unmount before teardown kept calling into the bridge for up to ~1.7s afterdispose()had already cleared every native listener. The retries are now cancelled bydispose(). - Per-widget notifiers were leaked when a key never got a slot. Both
adapters'
dispose()walked only the slot maps, but the per-key listenable bundles (and AppLovin's per-key ad-view-id notifiers) are created independently of the slot, so any key that only ever had those kept itsValueNotifiers alive for good.
-
2.3.222 Aug 2026Nothing published for this version
-
2.3.122 Aug 2026Release notes
Open source →Consent-path hotfix. Every item below was found by the round-5 audit and the first two were reproduced on real hardware (Pixel 7 Pro,
debugGeography: debugGeographyEea, real UMP forms) before and after the fix.Fixed
- Audit round 5 — the consent form was re-shown on every launch to an
EEA/UK user who had already answered it. The flow gated on
isConsentFormAvailable(), which reports whether a form exists, not whether consent is required — and a form stays available after consent, because that is what backs the Privacy Options entry point. Confirmed on a real device (Pixel 7 Pro,debugGeography: debugGeographyEea): a cold restart with consent already granted loggedstatus=obtained formShown=trueand put the form back on screen. Now uses Google's ownConsentForm.loadAndShowConsentFormIfRequiredbehind astatus == requiredguard, so an already-answered user is never asked again and the common (non-EEA) case skips the platform call entirely. - Audit round 5 — the consent gate could stay shut for a whole session
with no way to recover.
_umpAttemptFailedwasresult.error != nullalone, but UMP returnserror == nullwithcanRequestAds == falsewhenever it resolves from cache without being able to serve a form — the ordinary "flaky network on first launch in the EEA" case. Both retry paths gate on that flag, so the gate stayed closed for the rest of the process: zero ads, no self-heal short of an app restart, even once the network came back. It now also covers an inconclusive result and a still-closed gate. - Audit round 5 — the consent form gave the user only 20 s to answer.
The dismiss timeout was shared with the network steps, so a person reading
a real GDPR form (206 partners, an expandable "Learn more") had the flow
abandoned out from under them, resolving the ad gate before they had
chosen. Split out to 180 s for the human step; the no-network case is
still bounded by the 20 s guard on
requestConsentInfoUpdate, and a cap still exists so an unattended simulator cannot hang the flow forever. - Audit round 5 — the UMP retry paths could run several consent flows at
once, and dropped
tagForUnderAgeOfConsentwhen they did. Concurrent callers now join the in-flight request instead of presenting a second form and racing each other's writes to the gate; retries replay the params of the original call, so a child-directed app no longer collects consent through the wrong form (which would not have been valid for an under-age audience) and an EEA-debug run stays reproducible. - Audit round 5 — the periodic UMP backstop was unbounded. With the widened failure flag above, an EEA user who legitimately chose "reject" also reads as "gate closed", so the backstop would have re-run the consent flow every 5 minutes for the rest of the session. It is now capped, and never re-runs for a user UMP already got an answer from.
Added
example:--dart-define=UMP_EEA_DEBUG=true --dart-define=UMP_TEST_ID=<hash>drives the real EEA consent path on a test device. Without it a tester outside the EEA can never reach UMP'srequiredbranch, so every EEA-only code path stays unexercised — that blind spot is what let the two consent bugs above ship.UMP_TEST_IDis the hashed device id UMP prints to the log on first run.
- Audit round 5 — the consent form was re-shown on every launch to an
EEA/UK user who had already answered it. The flow gated on
-
2.3.022 Aug 2026Release notes
Open source →Added
AdMobConfig.effectiveTestDeviceIds/kQaTestDeviceHashes— this team's own QA device fleet's AdMob test-device hashes are now always merged intoRequestConfiguration.testDeviceIdson everyinitialize()/consent re-apply, regardless of what a host app configures intestDeviceIds. Keeps manual QA on real hardware from ever counting as real impressions/clicks (and the invalid-activity rate-limit risk that comes with it), without the host app having to know or maintain the list.
Fixed
- Audit fix —
initialize()'sautoRequestUmpConsentbranch could fail open on a real consent-fetch error, not just an unwired UMP channel. Any exception used to fail the gate open; now onlyMissingPluginException(channel genuinely not wired) fails open — every other exception (a real UMP fetch failure) fails closed, so a network hiccup can no longer silently ship ads with no verified consent decision. - Audit fix —
destroy()/_resetGuardState()left the previous session's device GAID behind. A stale GAID surviving past teardown into the nextinitialize()is a privacy leak; it's now cleared as part of guard-state reset. - Audit fix — reopening the
canRequestAdsListenablegate mid-session never triggered a frame inBannerAdWidget/MrecAdWidget/NativeAdWidget._onCanRequestAdsChanged()'s reload path relied onaddPostFrameCallback, which does not itself schedule a frame — the reload silently no-opped until some unrelated frame happened to fire. Fixed by callingWidgetsBinding.instance.scheduleFrame()alongside it. - Audit fix —
MonetizationArbitrator's with-estimator branch could veto ads at zero eCPM. The no-estimator branch already guarded onecpm > 0; the with-estimator branch was missing the same guard, so a session with no revenue events yet (ecpm == 0) could still have ads vetoed whenever the host's likelihood estimator reported > 0.5. Both branches now requireecpm > 0before vetoing.
-
2.2.020 Aug 2026Release notes
Open source →Added
AdManager().currentDeviceGaidandAdManager().adMobTestDeviceHashHint()— the latter returns instructions (device's current GAID included, clearly labeled) for finding this device's AdMob test-device hash via logcat tagAds, since Google has no public API/formula for that hash. Intended for a host app's own debug UI; distinct from the GAID, which is not valid for AdMob'sRequestConfiguration.setTestDeviceIds().
-
2.0.409 Aug 2026Release notes
Open source →Docs-only. No code changes. Prompted by an independent multi-agent audit (Claude/Codex/Gemini,
doc/audit/audit_*.md) flagging that the pubspec description overclaimed "Offline VIP redeem".Changed
- pubspec
description— "Offline VIP redeem" → "Offline-verified VIP codes". The Ed25519 signature check is fully offline, butredeemSignedKeyhas rejected the redeem attempt while offline since 2.0.1 (deliberate anti-abuse gate) — the old wording implied the whole flow works offline, which it hasn't since that release. - README — added a "Known limitation — redeem attempt requires connectivity" callout next to the signed-VIP-keys section, spelling out the same distinction.
- pubspec
-
2.0.309 Aug 2026Release notes
Open source →Docs-only. No code changes.
Added
example/README.md— a Quickstart section with the minimalsetNavigatorKey/navigatorObservers/requestUmpConsent/initialize/buildBannersnippet, so the pub.dev "Example" tab is self-contained instead of only linking out to the package README.
-
2.0.209 Aug 2026Release notes
Open source →Docs-only. No code changes.
Added
example/README.md— an index of the 16 demo pages inexample/lib/main.dart(one row per page: what it demonstrates), so the pub.dev "Example" tab has something to navigate besides a 2,500+ line raw file.
-
2.0.109 Aug 2026Release notes
Open source →Non-breaking bug fixes, cross-checked by three independent agents (Codex, agy, a second Claude instance) with every finding verified against the source. 698/698 tests pass; manually verified on a real Android device.
Fixed
AdManager.initialize()bounded auto-retry. A failed adapter init (bad ad unit ids, missing native config, transient SDK error) now retries up to 3 times with backoff (5s/15s/30s) before giving up for the session, instead of leaving the host permanently uninitialized until the next app launch or an explicit re-initialize()call.onCompletenow fires exactly once per host-initiatedinitialize()call. Previously it could fire on every failed attempt in addition to the terminal outcome (up to 4 times across the retry budget), violating the 1.x callback contract of firing once with the final result.- Stale internal-retry flag could leak into a later legitimate call. A
retry timer firing while another
initialize()call already held the busy guard left_isInternalInitRetryCallstucktrue, causing the next real host-initiated call to be misclassified as an internal retry. VipManagerclock-rollback guard applied consistently. The clock-rollback-resistant "now" getter (_effectiveNow, clamped against a persisted high-water mark) was already used for expiry/stacking calculations but was missed in_refreshGraceNudgeand_scheduleNextExpiry, which still read the raw device clock — a backward clock jump could desync the grace-nudge and next-expiry timers from the rest of the VIP state.
Changed
VipManager.redeemSignedKeynow rejects redemption attempts while the device is offline, returningVipRedeemStatus.invalidwith a "no network connection" message, before running Ed25519 signature verification. Deliberate anti-abuse tightening — a host at 2.0.0 that allowed a signed key to be redeemed while offline will see those attempts rejected at 2.0.1. Ed25519 verification itself is still fully offline (no server call, no shared secret); only the redemption attempt now requires connectivity.
Known limitations (unchanged, not new in this release)
VipManager.redeemVip's separate host-supplied-validator path is not gated by the offline check above — onlyredeemSignedKeyis. Consumers usingredeemVipwith their own validator should apply their own connectivity check if desired.AdManager.isConnectedoptimistically returnstrueif read before the connectivity watcher is ready, or if the platform check throws — a small fail-open window on cold start.
-
2.0.005 Aug 2026Release notes
Open source →Breaking. Comes out of a full audit against seven production requirements (
doc/audit/audit_claude_20260802.md), cross-checked by three independent agents, with every finding verified against the source.Breaking
autoRequestUmpConsentnow defaults totrue. With the old defaults (false, plusdisableAppLovinCmpFlow: trueandautoShowConsentDialog: true) a host that changed nothing tripped the consent-coverage footgun, which hard-blocks every ad request in a release build — and the built-in dialog could not clear the block, because it applies consent directly to the providers and never routes throughsetConsent(). The result was a release that requested zero ads, silently: theassertnext to the block is stripped in release, leaving one log line. Hosts that already callrequestUmpConsent()themselves are detected and the automatic call skips, so UMP still runs exactly once.maxVipStackDurationnow defaults to 90 days instead ofnull(uncapped). Passnullexplicitly for the old behaviour, knowing the only remaining ceiling is the ~100-year sanity bound in the key parser.- Signed VIP keys default to a new
AVP2format carrying an expiry and an app binding inside the signed payload.AVP1keys already issued still verify;tool/vip_mint.dartmints AVP2 unless--v1is passed. - New dependency:
package_info_plus, used to read the bundle id that AVP2 keys are checked against.
Fixed
- Interstitial and rewarded ads could stack on each other.
showAppOpenAdOnResumechecked the other two fullscreen slots and the dialog stack, butshowInterstitialandshowRewardedeach checked only themselves, so a call while another fullscreen ad was showing put one ad on top of another — an AdMob and AppLovin policy violation.AdSafetyConfig.canShowFullscreenAd()does not cover this: it is a time-based frequency gate, not a state mutex. All three paths now share one predicate. - Banner/MREC/native loads ignored the consent, VIP, cap and connectivity
gate — in both adapters. None of the ten load entry points consulted
canReload, so a resume after a banner error (or any other caller) could fire an ad request whilecanRequestAdswas false, while the user was VIP, or past the daily cap. Requesting an ad withcanRequestAds == falseis a UMP policy violation, and it was invisible from the UI because the widget layer hides banners for VIP users anyway. ThecanReloadseam existed onAdMobAdapterbut was dead code — only AppLovin ever called it. - A failed UMP attempt was never retried. On reconnect the SDK refilled ad slots but not consent, so an EEA user whose first launch had no network never saw a consent form for the rest of the process. Now retried on the offline→online transition, and only when the previous attempt actually failed, so a user who already answered is not asked again.
- A UMP status of
unknownsilently downgraded a stored consent.unknownmeans UMP could not determine anything, not that the user refused, but it mapped tohasUserConsent: falseand overwrote a choice the user had already made — visible in the logs asload → consent=truefollowed byset → consent=false. Inconclusive results now leave the persisted value alone.requiredstill maps tofalse: there the form is genuinely needed and was not completed. - The consent SDK could abort
initialize().requestConsentInfoUpdateis a callback API returningvoid; when the UMP channel is not registered it throws from a future nobody awaits, so the error escaped as an unhandled zone error that atry/catcharound the call could not catch. Unreachable while the default wasfalse; now contained.
Documentation
maxVipStackDuration's docstring claimed the non-stacking path was never clamped. It was wrong —VipManager.addViphas clamped both paths since the single-entry cap was added. (The year-2099 legacy-GAID migration grant really is exempt, but because it constructs itsVipEntrydirectly.)- README now states plainly that VIP anti-bypass is Keychain-durable on iOS and weak on Android, where clearing app data resets both the trial and key reuse, and that offline keys cannot be revoked.
- The example's demo keypair is now marked as public knowledge and unsafe to ship.
-
1.2.401 Aug 2026Release notes
Open source →Metadata only — no code, API or behaviour change from 1.2.3.
Changed
- Shortened the package
descriptionand all threescreenshots:descriptions to under 160 characters. pub.dev enforces two different limits and neither is reported bypub publish --dry-run: the upload API rejects anything over 200 characters, while pana's scoring wants under 160 or it drops 10 points from "Provide a valid pubspec.yaml" and another 10 from "Package has an example and has no issues with screenshots". 1.2.3 uploaded fine at 187-197 characters but scored 130/160 for that reason.
- Shortened the package
-
1.2.301 Aug 2026Release notes
Open source →Fixed
autoRequestUmpConsentwas never honoured duringinitialize()— a host that opted into automatic UMP now actually gets the consent request before ad requests start (R10-A).- A COPPA flag set mid-session now hard-stops AppLovin ad requests instead of only applying to the next SDK init (R10-B).
_retryRefillAds()returns immediately while the device is offline, instead of burning retry budget on requests that cannot succeed (R10-C).ConnectionNotifierTools.initialize()is bounded by a 20s timeout, so a hung connectivity plugin can no longer stall SDK init indefinitely (R10-D)._footgunBlockedleaked across re-init: one release-modeinitialize()could permanently block ads for every later init in the same process. The same bug class then recurred for_umpRequested/_consentExplicitlySet, sodestroy()and the re-init branch now share one_resetGuardState()instead of two hand-maintained reset lists.applyDryRunReleaseGuard()'sisReleaseis threaded into the last two call sites (thead_manager.dartconsent-footgun guard and theVipManagerconstructor) that still fell back to rawkReleaseModeunderflutter test.
Changed
- Example app now mirrors the host's Android Auto Backup configuration, so the VIP-reinstall-replay path behaves the same in the example as in production.
SafeLogger:critical()ande(bypassLevel: true)consolidated onto one internal_e();_shouldLog'sbypassLevelbranches merged.VipManager'sisReleaseparameter is no longer@visibleForTesting(mirrorsad_safety_config.dart— the safety comes fromisActuallyRelease(), not from a compile-time restriction).- Added
repository/homepage/issue_tracker/topicsand an explicitplatforms: android, iosto the pubspec; whole package reformatted withdart format. No API or runtime change.
Documentation
- Explained why interstitial and rewarded ads intentionally have no watchdog, unlike App Open (R10-E).
-
1.2.220 Jul 2026Release notes
Open source →Changed
SafeLogger's default log level is nowkDebugMode-based (verbose in debug, warning-and-above in release) instead of always-verbose — a host that never callsAdManager.setLogLevel()no longer leaks raw GAID and other diagnostic detail into release logs by default.- The consent-coverage footgun (AppLovin CMP disabled +
autoRequestUmpConsentfalse +requestUmpConsent()never called beforeinitialize()) now hard- blocks ad requests in release builds (kReleaseMode), not just a dev-timeassert()(which strips in release and was previously log-only in production). The block clears automatically the momentsetConsent()is called — directly by a host's own consent UI or internally byrequestUmpConsent()— and triggers a refill of any ad slots held back while it was active.
Fixed
NativeAdWidget'sMaxNativeAdViewlistener callbacks now checkadapter.isInitialisedbefore writing to itsValueNotifiers or firing a click event, closing the same disposed-adapter race already guarded on the AppLovin banner/mrec views.
-
1.2.119 Jul 2026Release notes
Open source →Added
VipManager.firstInstallGrantDueListenable— fires once when the first-install VIP grace window is granted (previously silent/log-only), paired withlastFirstInstallGrantDurationandacknowledgeFirstInstallGrant(). Mirrors the existinggraceNudgeDueListenablepattern.AdManager.initialize()now callsnotifyFirstInstallGrant()right after granting the window.
-
1.2.019 Jul 2026Release notes
Open source →Added
RevenuePanelgained an optionaldebugModeOverrideconstructor param (test-only seam,@visibleForTesting) so the widget'skDebugModegate can be exercised fromflutter test.
Changed
SimpleEventBusnow replays the last-fired event to a listener that subscribes after the event already fired, closing a gap where late subscribers silently missed init-completion signals.clearAll()(called fromAdManager.destroy()) resets the replay buffer.RevenuePanelnow fully gates onkDebugMode(or the override above): no event subscription andSizedBox.shrink()render in release builds, instead of only skipping the visual chrome.
Fixed
ad_manager.dartescalates the existing silent log warning for a misconfigured consent flow (AppLovin CMP disabled,autoRequestUmpConsentfalse,requestUmpConsent()never called beforeinitialize()) to a dev-timeassert()— asserts strip in release, so production behavior is unchanged, but dev/test builds now fail loudly instead of silently shipping with no consent flow.requestUmpConsent()now logs a warning if called beforerequestAtt()on iOS (ATT must run first per platform policy) — log-only, non-blocking.
Docs
- Clarified in the README: the AdMob-per-request-tag vs. AppLovin-full-abort
COPPA asymmetry is intentional (each provider's native API surface
differs), not an inconsistency;
enableFillRateMonitor/enableArbitratorare production-safe opt-in tools with nokDebugModedistinction; UMP→ AppLovin consent sync is boolean-only by design since AppLovin MAX SDK 12.0.0+ auto-reads the IAB TC-String directly; pointers to the existing CCPACupertinoSwitchpattern andconsent_dialog.dart's binary-only rationale for hosts that need more UI; noted the Android VIP-key reinstall-replay limitation. example/ios/Runner/Info.plistsynced from 50 → 152SKAdNetworkItemsentries to match the host app.
-
1.1.118 Jul 2026Release notes
Open source →Changed
- Bumped
confetti^0.7.0→^0.8.0andconnection_notifier^2.0.1→^4.1.0(dependency freshness, closes Pub Points "up-to-date dependencies" gap). No API surface used by this package (ConnectionNotifierTools .initialize()/.isConnected/.onStatusChange) changed acrossconnection_notifier's 3.x/4.x majors — those breaking changes only affected its widget/UI layer, which this SDK doesn't use.
- Bumped
-
1.1.018 Jul 2026Release notes
Open source →Added — Native Ad format v1 (
buildNative())- New
AdSlotType.native+AdScreen.buildNative(). AdMob renders via Google'sNativeAd/NativeTemplateStyle(templateType: TemplateType.medium)(same preload-then-AdWidgetpattern as banner/MREC; the template self-draws its own "Ad"/AdChoices label). AppLovin renders via a self-containedMaxNativeAdViewwith a custom Dart child layout (MaxNativeAdIconView/TitleView/MediaView/BodyView/CallToActionView), for which the package self-draws its own "Ad" compliance badge (mirrors MREC's_MrecContainerbadge). v1 ships one fixed layout — not a customizable editor. See README § "Native Ad (v1)".
Added — MREC ad format (
buildMrec())- Medium-rectangle banner variant (
AdSlotType.mrec), same lifecycle shell as banner (RouteAware pause/resume, VIP suppression, offline collapse, auto-refresh gate).
Added — Smart Monetization Arbitrator + fill-rate monitor
monetization_arbitrator.dart: opt-in per-slot provider arbitration with a guardrail against flapping between providers.fill_rate_monitor.dart: tracks per-slot fill rate over a rolling window for arbitration decisions and diagnostics.
Added — Mediation waterfall reporting
- Adapters now surface mediation waterfall/network response data through the
existing
AdEventstream for host-app-side analytics.
Added — Consent-country analytics
- Consent events now carry the resolved consent country (GDPR/CCPA scope) so host apps can break down consent-rate metrics by region.
Added — Config validation preflight
ad_diagnostics.dart/integration_self_check.dartgained checks that catch common misconfiguration (missing ad unit IDs, mismatched provider config) before the SDK starts requesting ads.
Fixed — [High]
AppLovinAdapter.preloadMrec()crashed the host app when MREC wasn't configuredAdManager.initialize()(and the VIP-loss handler) unconditionally preload the MREC slot alongside banner, regardless of whether the host app actually usesbuildMrec(). For AppLovin, an unconfigured MREC resolvesAppLovinConfig.mrecIdto its default empty string, and AppLovin's nativeMaxAdViewImpl.loadAd()throwsIllegalArgumentException: No Ad Unit ID specifiedsynchronously inside an AndroidHandlercallback — outside Dart's platform-channel try/catch, so it crashed the whole process instead of surfacing as a catchable Dart error. Any AppLovin-provider host app that doesn't configure MREC (i.e. virtually all apps, since MREC only shipped this release) would crash on every SDK init.preloadMrec()now skips the native preload entirely whencfg.mrecIdis empty.
Fixed — [High]
AppOpenTrigger.splashOnly/resumeOnlyonly gated the SHOW path, not LOADshowAppOpenAd/showAppOpenAdOnResumerespectedappOpenTrigger, butloadAppOpenAd()(init/VIP-change/retry-refill) never checked it — undersplashOnly/resumeOnlythe App Open slot kept getting refilled and could sitreadyindefinitely (AppLovin has no AdMob-style 4h TTL), wasting network requests/fill quota. Defaultbothwas unaffected (both gates are no-ops there). Now gated behind the sameappOpenTriggercheck as the show path.
Fixed —
redeemVip()demo mode (validator == null) no longer accepts any key in release builds- Legacy
redeemVip()accepts any key whenAdConfig.vipKeyValidatorisnull, intended as a zero-config demo mode. A host app that forgot to wire a validator would ship this silently — any user typing any string got free VIP._runValidatornow refuses (return false) whenvalidator == nullandkReleaseModeis true; debug/profile builds keep the original demo-mode passthrough so wiring the integration still works without a validator during development. Does not affectredeemSignedKey()(Ed25519-verified, the path production code actually uses).
- New
-
1.0.2416 Jul 2026Release notes
Open source →Published in two commits the same version: 2026-07-10 (consent-on-init + ATT/UMP timeout fixes below) then 2026-07-16 (privacy-options timeout + CCPA toggle + SKAdNetwork expansion + doc fixes). Both are live on pub.dev under
1.0.24— the split below is historical, not two releases.Fixed —
requestPrivacyOptionsFlow()could hang forever on a served-but-never-dismissed form (T44)- The native "Privacy Options" form's dismiss callback only fires after
ConsentForm.showPrivacyOptionsForm()'s own platform call resolves; awaiting that call directly (as the code did) meant a stuck/never-dismissed form hung the whole call with no way out — bypassing an already-added completer timeout that could never be reached. Fixed by not awaitingshowPrivacyOptionsForm()directly (matching the existing fire-and-forget shape already used forrequestUmpConsentFlow()'s form show/dismiss) and applying the 20s timeout to the dismissCompleterthat its callback feeds. Test:test/ump_consent_test.dart.
Added — CCPA "Do Not Sell or Share My Info" toggle on
VipRedeemScreen- New optional
doNotSellValue/onDoNotSellChangedparams render a switch in the privacy footer (next to Privacy Policy/Privacy Options), letting a host app wire it straight toAdManager().setConsent(AdConsent(doNotSell: ...)). Opt-in only — omittingonDoNotSellChanged(the default) renders the same footer as before.
Docs
- Added a "Known limitations — read before adopting" section to the README (ad-policy risk sits with AppLovin/Google, not this package; the real ad show/dismiss lifecycle is only partially automatable — 3/15 integration_test scenarios are manual-only; limited real-world production history beyond this repo's own host app; single maintainer, no SLA). Written for anyone evaluating this SDK for a new app/partner before a wholesale integration.
Fixed — auto-reload paths bypassed VIP/consent/cap/connectivity gates
- App-Open, Interstitial, and Rewarded adapters (
applovin_adapter.dart) all refill ads directly from their ownonAdHidden/onAdDisplayFailednative callbacks, bypassingAdManager.loadX()and therefore its gates entirely. A user who just redeemed VIP, went offline, or revoked consent could still trigger an outbound ad request from a stale in-flight callback. Fixed by adding acanReload()check (AdManagerwires it to!_isVipMember && !AdSafetyConfig.dailyCapReached() && _canRequestAds && isConnected) immediately before every such reload call site.
Fixed — consent silently overwritten by stale data on every
initialize()AdManager.setConsent()called beforeinitialize()(the real app startup order:requestUmpConsent()→initialize()) used to only mutate an in-memory field and return early, becauseConsentManagerwasn't bootstrapped yet.initialize()'s subsequentConsentManager.bootstrap()then unconditionally reloaded the previous session's stale persisted consent and overwrote it — silently discarding the fresh UMP result on every app launch. Fixed by buffering the pendingConsentSettingsin a new_pendingConsentSettingsfield and re-applying it right after bootstrap, so it wins over the just-loaded stale data; the buffer is cleared ondestroy()so it never leaks into an unrelated futureinitialize(). Test:test/consent_persistence_on_init_test.dart.
Fixed — ATT/UMP consent native awaits could hang
initialize()forever (T43)requestAttIfNeeded()andrequestUmpConsentFlow()each awaited a native modal-dismiss (or network) callback with no timeout. If the OS/native side never resolved it (ATT prompt throttled by rapid repeated launches; a UMP form served but never tapped through; a dead network onrequestConsentInfoUpdate), the wholeinitialize()chain never ran — the ad SDK stayed silently uninitialized for that app session. All three awaits are now wrapped inFuture.timeout(Duration(seconds: 20), onTimeout: () => <safe fallback>), mirroring the existing App-Open watchdog pattern.requestPrivacyOptionsFlow()'s dismiss await is deliberately not part of this fix — it's a user-initiated re-consent action outside the app-boot gating chain. Tests:test/att_consent_test.dart,test/ump_consent_test.dart(fakeAsync+ never-completingCompleter, mocking the realgoogle_mobile_adsUMP method channel with its customStandardMethodCodec(UserMessagingCodec())).
Added —
ssvUserId/ssvCustomDataonAdScreenState.showRewardedAd()AdManager.showRewardedAd()already acceptedssvUserId/ssvCustomDatafor server-side reward verification, but theAdScreenState.showRewardedAd()convenience wrapper (lib/src/core/ad_screen.dart) that most host screens actually call did not expose or forward them — any caller passing those named args failed to compile. Added both as optional parameters, forwarded as-is to the underlyingAdManagercall; no change to the wrapper's existing safety-check/disclosure-dialog behaviour.
Added — durable redeemed-key ledger for signed VIP keys on iOS
- New
RedeemedKeyLedger(lib/src/vip/_redeemed_key_ledger.dart) backsVipManager.redeemSignedKey's one-time-use check with an iOS Keychain entry, alongside the existingAdPreferences(SharedPreferences) check.AdPreferencesalone is wiped on uninstall, so a user could uninstall/reinstall to redeem the same signed key repeatedly; the Keychain entry survives that. Android intentionally has no durable backstop here, same reasoning as the existingFirstInstallGuard(no local primitive survives uninstall without an install-referrer plugin for a narrow benefit) —AdPreferencesremains the sole check there. Fails open: any Keychain read/write error is swallowed and treated as "not redeemed" so a storage hiccup never locks out a legitimate key. Test:test/redeemed_key_ledger_test.dart.
Added — VIP grace-period expiry nudge
VipManagerexposesgraceNudgeThreshold(default 24h),graceNudgeDueListenable, andacknowledgeGraceNudge(). Once a VIP entry'sexpiresAtcomes within the threshold, the nudge notifier flips true so the host UI can prompt the user to redeem/extend before ads resume; acknowledging persists the currentexpiresAtso the same expiry doesn't re-nudge, but stacking a new expiry (redeem/watch-ad-to-extend) makes it due again. Inactive/no-VIP state is never due. Test:test/vip_manager_grace_nudge_test.dart.
Added — VIP entries integrity checksum
AdPreferences.getVipEntriesRaw()/setVipEntriesRaw()now store an FNV-1a checksum alongside the VIP entries JSON, as a single combinedSharedPreferencesvalue ('<checksum>|<json>'written via onesetStringcall). A mismatched checksum is logged and treated as absent data, deterring casual on-device editing of the plaintext VIP entries to self-grant free ad-free time — this is a tamper deterrent, not root/jailbreak-proof protection (a rooted device can still recompute the checksum). Pre-upgrade data with no checksum is trusted once and backfilled into the new format. FNV-1a was chosen overString.hashCode(not stable across Dart/Flutter versions) and over the existing asynccryptography-package HMAC (would force every VIP-entries caller async). Note: an earlier two-separate-keys design was replaced with the single combined key above after it surfaced a real race — a concurrent fire-and-forgetVipManager._save()write could be observed mid-flight with one key updated and the other still stale, causing a false "tampered" read. Test:test/ad_preferences_test.dart.
Changed — native ad SDK dependency pins retested, still blocked upstream
- Retested bumping
applovin_max/gma_mediation_applovinto the latest upstream versions (4.6.4/2.6.1) to see whether the CocoaPods version-pin conflict documented in the hostpubspec.yamldependency_overrideshad been resolved. It has not:2.6.1now requiresmeta ^1.17.0, whileflutter_testfrom the CI-pinned Flutter SDK (3.35.1) forcesmeta 1.16.0— a Dart-level version-solve conflict, never even reaching the CocoaPods layer. No SDK code change; the pins stay atapplovin_max 4.6.0/google_mobile_ads 6.0.0/gma_mediation_applovin 2.5.1in the host app. Seedoc/audit/audit_partner_lead_20260710.mdfindings #2/#3.
Added — Privacy Options footer button on VipRedeemScreen (T28)
VipRedeemScreengainedonPrivacyOptionsTap(VoidCallback?) andVipRedeemStrings.privacyOptions, mirroring the existingonPrivacyPolicyTap/privacyPolicypair. The footer now renders whichever of the two buttons has a non-null callback (previously only the privacy policy button existed). Closes the gap whereAdManager().showPrivacyOptions()(T06) had no host call site — GDPR requires a durable re-consent entry point, not just a one-time policy link. Test:test/vip_redeem_screen_test.dart(footer hidden/shown/tap cases foronPrivacyOptionsTap).
Added — rewarded disclosure hook on
AdScreenState.showRewardedAd(T22)showRewardedAdgained optionaldisclosureTitle/disclosureSubtitle/disclosureButtonLabel/disclosureCancelLabelparams. WhendisclosureTitleis set, a confirm dialog naming the reward is shown right before the ad plays; declining callsonEarnedReward(false)and never reaches the ad. Omitted (default): behaviour is unchanged — return type changedvoid→Future<void>, non-breaking via Dart's void-return covariance. Test:test/ad_screen_test.dart(rewarded disclosure hookgroup — confirm/cancel paths).
Added — load-time daily safety cap gate (T21)
AdSafetyConfig.dailyCapReached()is a new pure read-only check (no CTR-anomaly side effects, unlikecanShowFullscreenAd()).loadAppOpenAd/loadInterstitial/loadRewardedAdand the periodic_retryRefillAdsscan now skip preloading once the daily fullscreen-ad cap is hit — previously the cap was only enforced at show time, so a capped-out user kept burning ad-network load requests that could never convert. VIP members are unaffected (the existing VIP guard already returns before this check runs in every call site). Test:test/daily_cap_load_gate_test.dart,test/ad_safety_config_test.dart(dailyCapReachedgroup).
Fixed — trial hardening: anti clock-rollback + grace-disabled footgun (T17)
VipEntry.isActive/remainingnow also checknow.isBefore(grantedAt)— previously onlynow.isBefore(expiresAt)was checked, so rolling the device clock backwards past a grant'sexpiresAtmade an already-expired-by-real-time entry "come back to life". A rolled-back clock is now treated as the entry having already been consumed (fail-safe), not as extra time granted.VipManager's purge/active/ stacking logic needed no change — everything already routes throughVipEntry.isActive. Test:test/vip_entry_test.dart(anti clock-rollback (T17)group).AdManager.releaseFootgunWarningsnow also warns (release builds only, same log-ERROR +assert(false, ...)treatment) whenAdConfig.firstInstallVipGraceis.disabled— previously a partner could silently ship with no ad-free first-install trial. Test:test/ad_manager_core_test.dart(firstInstallVipGracecases in thereleaseFootgunWarningsgroup).
Added — ad-unit-id validation in release footguns (T16)
AdManager.releaseFootgunWarningsnow also warns (release builds only, same log-ERROR +assert(false, ...)treatment as the existing dryRun/ Google-test-id guards) when: any resolvedbannerId/interstitialId/appOpenId/rewardedIdis empty, or (AdMob provider only) an id doesn't match AdMob'sca-app-pub-<16 digits>/<ad-unit id>format — the classic "pasted an AppLovin id into the AdMob config" mistake. Test:test/ad_manager_core_test.dart(releaseFootgunWarningsgroup).
Added — per-platform ad-unit ids (T15)
AdMobConfig/AppLovinConfiggained optionalandroid*Id/ios*Idoverrides forbannerId/interstitialId/appOpenId/rewardedId(e.g.androidBannerId,iosBannerId). Resolved viaPlatform.isAndroid/Platform.isIOSat read time, falling back to the existing single id when no override is set — fully backward compatible. Test:test/ad_config_platform_test.dart.
Added — Privacy Options entry point + re-consent (T06)
AdManager().isPrivacyOptionsRequired()andAdManager().showPrivacyOptions()(wrappingConsentInformation.getPrivacyOptionsRequirementStatus+ConsentForm.showPrivacyOptionsForm) are now documented in the README as the required durable re-consent entry point Google's UMP policy mandates (a permanent "Privacy Settings" button).showPrivacyOptions()safely no-ops (no native UI) when Google doesn't require it for the current user, and re-applies the resulting consent to the active ad provider (npa/RDP) when it does. Test:test/privacy_options_test.dart— required→opens form, notRequired→no-op, re-consent re-applies to the active adapter.
Added — shared
VipRedeemScreenwidget- Extracted the full VIP redeem screen (hero status, key input, watch-ad-extend,
active-entries list, buy placeholder, confetti) into the SDK as a reusable
VipRedeemScreen+VipRedeemStrings(localizable, mirrors theConsentDialogStringspattern). The host and the SDK example now render the identical screen — host injects Vietnamese strings, the example uses the English defaults. Privacy-policy opening is a callback (onPrivacyPolicyTap) so the SDK needs nourl_launcherdependency;confettiis added. - Widget tests: renders inactive state + privacy-footer visibility. Verified on a Samsung S24 Ultra.
Fixed — code-review follow-ups
- VIP:
redeemSignedKeynow claims the key id atomically (synchronous check + in-flight set) so a concurrent double-tap of the same signed key can't slip past the one-time-use check and grant twice. Enforced in the SDK, not just the host UI. Test: concurrent double-redeem grants exactly once. - Consent:
initialize()logs a loud runtime warning when AppLovin's CMP is disabled ANDautoRequestUmpConsentis false ANDrequestUmpConsent()was never called before init — the "no consent form anywhere" footgun. Runtime, not config-static, so it never false-alarms hosts that gather consent in their splash.
Improved — offline/network UX (T09 + T10)
- T09: verified the banner collapses to a zero-size box when offline (no
shimmer / battery drain) and reloads automatically on reconnect (via the
T08 connectivity watch bumping
initRevision). Added a widget test. - T10:
isConnectednow falls back to the last-known connectivity state (from the T08 watch) with a warning log instead of a silenttruewhen the detector is unavailable. Kept optimistic on purpose — a broken detector must not permanently block ads; genuine offline loads just fail and back off, and the watch refills on reconnect (network-error fast-retry is subsumed by T08).
Hardened — lifecycle & memory (T11 + T12 + T13)
- T13:
AdLoadingDialog.resetState()(called byAdManager.destroy()) now pops a still-showing dialog before clearing its flags, so a mid-dialog destroy / re-init can't strand a non-dismissable loading dialog on the navigator. (_eventStreamis intentionally left open — it's a process-lifetime singleton broadcast exposed publicly; closing it would break host subscribers and it is bounded to one instance, so it is not a leak.) - T11: added regression tests proving the fullscreen single-use guard — a
second show while one is showing is rejected by the slot state machine
(
isReady+ atomicbeginShow+ null-on-dismiss), a disposed ad is never re-shown, and dispose happens exactly once. (No code change needed; the guard already existed — the tests lock it down.) - T12:
BannerAdWidgetnow guards against stacking multiple post-frame_initBannercallbacks (_initScheduled) whenbuildruns repeatedly, so a banner loads exactly once across rebuilds. (loadBannerIfNeededalready bailed on a cached ad; this removes the wasteful callback pile-up.) (dispose-before-recreate was already handled by that early-return.)
Added — consent gate + UMP as single CMP (T01 + T03)
AdManager.canRequestAdsconsent gate: every load path (app-open, interstitial, rewarded, banner) AND every show path now skips when consent hasn't been granted, mirroring Google UMP'sConsentInformation .canRequestAds().requestUmpConsentstores the result and, when the gate opens (blocked→allowed), refills the held slots. Google policy: never request or show an ad whilecanRequestAdsis false. Defaultstrueso non-UMP / non-EEA hosts are unaffected.AdConfig.autoRequestUmpConsent(default false): when true,initialize()runs UMP before the first ad request and gates on the result — the SDK owns the whole consent flow.umpTagForUnderAgeOfConsentforwards the under-age flag.AdConfig.disableAppLovinCmpFlow(default true): the AppLovin adapter disables AppLovin's own Terms & Privacy (CMP) flow so UMP is the single consent prompt — no double prompt. UMP's result is still forwarded to AppLovin viasetHasUserConsent.- T03: the splash App Open ad (even
bypassSafety: true) no longer shows an impression before consent is resolved; the show gate also prevents a previously-loaded ad from showing after consent is revoked.
Added — offline signed VIP keys (T18)
- New
verifySignedVipKey+VipManager.redeemSignedKeyverify Ed25519-signed keys offline against an embedded public key. Only the public key ships, so a decompiler cannot forge new keys (the old local base64 map could be extracted and reused infinitely). VIP duration is encoded in the key. - Per-device one-time-use: a redeemed key id can't be redeemed again on the same
device (
AdPreferencesredeemed-id store). Global one-time-use still needs a server — documented as a known offline limitation. - New deps:
cryptography(pure-Dart Ed25519). Tooling:tool/vip_keygen.dart(generate a key pair) andtool/vip_mint.dart(mint signed keys with the private key — never shipped). See README → "Signed VIP keys". - Host
vip_keys.dartnow holds only the public key + demo keys;vip_screenredeems viaredeemSignedKey.
Added — connectivity auto-refill on reconnect (T08)
- The SDK now initialises
ConnectionNotifierTools(nobody did before, soisConnectedsilently always returnedtrueand the offline guards never fired) and subscribes toonStatusChange. - On an offline→online transition the SDK refills idle/cooldown ad slots,
nudges the banner preload, and bumps
initRevisionso banner widgets re-init — within ~1s (debounced) instead of waiting up to 5 min for the poll timer. Suppressed for VIP members and while uninitialised. Subscription cancelled ondestroy. Test seams:debugConnectivityChanged,debugReconnectDebounce.
Fixed — AdMob non-personalized ads (
npa) now actually applied (T02)- Previously
applyConsentToProvidersonly set AdMob's globalRequestConfiguration(COPPA/age tags) and never attached the per-request non-personalized flag, so a user who declined consent could still be served personalized AdMob ads. The doc comment claimed annpaextra was forwarded, but no code did so. AdProviderAdaptergainsapplyConsent(AdConsent).AdMobAdaptermaps!hasUserConsent→AdRequest(nonPersonalizedAds: true)on every load (banner, interstitial, rewarded, app open); it defaults to non-personalized until consent is applied and resets to that ondispose.AppLovinAdapter's implementation is a no-op (it forwards consent via staticAppLovinMAXAPIs).AdManagercallsapplyConsenton the adapter at init, fromsetConsent, and on anyConsentManagerchange (auto dialog / set / reset / privacy screen), so personalization tracks consent across every path.- Tests: adapter-level npa propagation + AdManager wiring (integration) + UI-driven consent (widget). Example app shows a live "personalized vs non-personalized" indicator on the Consent page.
- The native "Privacy Options" form's dismiss callback only fires after
-
1.0.2315 Jun 2026Release notes
Open source →Changed — App Open ad never stacks on top of a modal
AdScreenRouteLoggernow tracks how manyPopupRoutes (dialogs, bottom sheets, Cupertino popups) are on the navigation stack and exposesAdScreenRouteLogger.isDialogOnTop.showAppOpenAdOnResumeconsults it (plusAdLoadingDialog.isShowing) and skips the App Open ad while any dialog is presented — e.g. the consent dialog or a VIP redeem confirmation. Showing a fullscreen ad over a modal is bad UX and an AdMob policy risk. The counter is reset byAdManager.destroy()so a mid-dialog teardown can't wedge it.
Fixed — retry-refill scan bails early for VIP members
_retryRefillAdsnow returns immediately when the user is a VIP member. Eachload*()already guarded on VIP, so behaviour is unchanged, but this is a defense-in-depth backstop and avoids a pointless periodic scan/log.
-
1.0.2014 Jun 2026Release notes
Open source →Example only
- The bundled example (
example/lib/main.dart) now demonstrates the recommended consent ordering in its splash:AdManager().requestAtt()→AdManager().requestUmpConsent()→AdManager().initialize(). No library / public-API change vs 1.0.19 — upgrading requires nothing.
- The bundled example (
-
1.0.1914 Jun 2026Release notes
Open source →Added — iOS App Tracking Transparency
AdManager().requestAtt()/requestAttIfNeeded()— show the iOS ATT prompt when needed and return a structuredAttResult { status, idfa, allowsTracking }(AttStatusenum). No-op on Android; never throws (degrades todenied). Call it in the splash beforerequestUmpConsent. RequiresNSUserTrackingUsageDescriptioninInfo.plist. Decoupled from the GDPR consent flag — the native SDKs read ATT directly for IDFA.
Fixed
- iOS App Open watchdog false-positive — the lifecycle-aware show timeout no
longer force-dismisses on iOS, where the ad shows while the app stays
resumed. The "foreground = hung" heuristic is now Android-only; iOS relies on the native hidden/displayFailed callbacks plus the 90 s hard cap. - AppLovin reload-after-display-fail — a slot is no longer stranded by the
backoff window after a show failure; it refills immediately via the new
AdSlot.beginReload()(genuine load failures still back off). - AdMob parity — App Open now has a 90 s show watchdog; interstitial/rewarded
honour a 1 h freshness expiry; the banner slot transitions to
loadingbefore the nativeBannerAdis created (fixes a synchronous-fill race).
Internal
- Both adapters now load through an injectable bridge (
AppLovinBridge/GmaBridge) for full behavioural unit-test coverage. No public-API change.
Compliance / docs
- Removed the rewarded→interstitial reward fallback (rewarded-policy compliance); removed the interstitial on "Start" actions in the example host.
Upgrading from 1.0.18 requires no code changes for existing integrations. To use ATT, add
NSUserTrackingUsageDescriptionand callAdManager().requestAtt(). -
1.0.1827 Apr 2026Release notes
Open source →No code changes
- Version bump only. Runtime behaviour, public API surface, and bundled
assets are identical to 1.0.17. Upgrading from 1.0.17 to 1.0.18
requires no code changes — only a
pubspec.yamlversion bump andflutter pub get.
- Version bump only. Runtime behaviour, public API surface, and bundled
assets are identical to 1.0.17. Upgrading from 1.0.17 to 1.0.18
requires no code changes — only a
-
1.0.1727 Apr 2026Release notes
Open source →Added — Anti-uninstall-bypass for first-install VIP grace (iOS-side)
FirstInstallGuard(internal,lib/src/vip/_first_install_guard.dart) — protects thefirstInstallVipGracefeature against the trivial bypass of "uninstall + reinstall to claim a fresh 24-hour grace window." Wired automatically insideAdManager.initialize; host apps need no code changes.- iOS defence — writes a single boolean flag to the iOS Keychain
(
kSecAttrAccessibleAfterFirstUnlock, nosynchronizable, nokSecAttrAccessGroup). Keychain entries persist across app uninstall by default on iOS, so a reinstall on the same device finds the flag and the guard skips re-granting. Deliberately uses a constant flag rather thanidentifierForVendor(IDFV) — Apple resets IDFV when the user deletes all of a vendor's apps and reinstalls, which would let a standalone-app reinstall silently bypass the guard. - Android defence (host-app responsibility) — there is no reliable
local-only Play Install Referrer signal that distinguishes a fresh
install from a reinstall (per Google's docs, referrer info is reset
when the application is reinstalled). Real Android anti-bypass relies
on the host app's Auto Backup configuration restoring
FlutterSharedPreferences.xml(which contains theprefs.isFirstInstallGraceApplied()flag) on Play Store reinstall, short-circuiting the outer grace block before the guard runs. The guard itself returnsfalse(allow grace) on Android — anti-bypass is performed entirely by the host'sAndroidManifest.xml/<data-extraction-rules>+ Google Cloud Backup. - Call-order guarantee (iOS) —
AdManagerwrites the Keychain anti-bypass flag before theprefs.markFirstInstallGraceApplied()flag, so a process kill between the two writes leaves the persistent marker set and the next install on the same device is still blocked. - Debug bypass —
kDebugModebuilds skip bothhasAlreadyGrantedandmarkGranted, so QA can iterate onflutter runwithout the Keychain signal locking them out of the grace UX. Anti-bypass validation must happen on signed release builds (TestFlight / Play Store internal track). - Fail-open philosophy — every storage error is caught and logged;
the guard returns
false(allow grace) so a transient Keychain hiccup never denies grace to a legitimate first-time user. - 15 new unit tests covering debug bypass, Keychain present/absent/
tampered/error, Android always-grant behaviour,
markGrantedno-op on Android, idempotency, and fail-open error swallowing.
Changed
- New required dependency for the iOS guard:
flutter_secure_storage: ^9.2.4— iOS Keychain wrapper.
- Approximate binary size delta: +400 KB (Keychain wrapper native code).
Host-app integration notes
- Android (required for anti-bypass) — add Auto Backup configuration
to
android/app/src/main/AndroidManifest.xml:
Create<application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/full_backup_content">android/app/src/main/res/xml/data_extraction_rules.xml(Android 12+) andfull_backup_content.xml(Android 6-11) includingFlutterSharedPreferences.xmlso Google Auto Backup restores the grace flag on Play Store reinstall. Without these, Android anti-bypass does not work — uninstall + reinstall always re-grants the grace window. (Acceptable for many apps; configure Auto Backup only if you want to block this bypass.) - iOS — no host-side configuration required.
Removed
play_install_referrerdependency — initially included for an Android conservative-skip path, removed after research confirmed Install Referrer cannot detect Play Store reinstall (timestamps reset per Google's documented behaviour). Real Android anti-bypass comes from Auto Backup, not Install Referrer.
Limitations (documented, not fixed)
- iOS factory reset ("Erase All Content and Settings") wipes Keychain → bypass succeeds. Acceptable; factory resets are rare.
- Android Play Store reinstall without Auto Backup or within Auto Backup's ~24 h cache window still bypasses the guard. This is a fundamental local-only limitation — closing it requires a backend (Firebase Anonymous Auth + Firestore, or a custom server).
- iOS encrypted backup restore to a new device could carry the Keychain flag onto the new device, denying that device's first install grace. Edge case; acceptable trade-off vs. weakening anti-bypass on the primary device.
-
1.0.1626 Apr 2026Release notes
Open source →Documentation
- Full English rewrite of
README.md— restructured into 13 sections with table of contents. Quick start expanded into 6 copy-paste steps any Flutter developer can follow without prior AdMob/AppLovin knowledge. Added complete public API reference, FAQ, and dedicated Pitfalls section covering theandroid:taskAffinity=""issue (the most common cause of perceived crashes during background → foreground ad cycles). - Full English rewrite of
MIGRATION.md— clear 1.0.14 → 1.0.15 upgrade path (no breaking changes), plus legacy 1.x → 2.x path with auto-migration details. Added Common issues and FAQ sections. - Full English rewrite of
doc/architecture.md— deep-dive for contributors and advanced integrators. Added detailed sections on the Smart App-Open timeout, Slot-state dismiss watcher, Consent flow sequence, Memory management contract, and Manifest pitfalls.
No code changes
- This is a documentation-only release. The runtime behaviour, public
API surface, and bundled assets are identical to 1.0.15. Upgrading
from 1.0.15 to 1.0.16 requires no code changes — only a
pubspec.yamlversion bump andflutter pub get.
- Full English rewrite of
-
1.0.1526 Apr 2026Release notes
Open source →Fixed
- AppLovin App-Open timeout false-positive — old fixed 10 s timeout fired
while user was still interacting with the ad (click → browser → return),
marking dismiss with
falseand arming the resume guard prematurely. Replaced with lifecycle-aware polling: re-arms every 5 s while app is paused (= ad still showing), force-dismisses only when app is foreground for 2 consecutive ticks withoutonAdHiddenCallback(hard cap 90 s). - Banner paid-event not wired —
BannerAdextendsAdWithViewwhich has noonPaidEventsetter; previous dynamic dispatch silently failed. Banner revenue is now correctly emitted viaBannerAdListener.onPaidEventconstructor parameter. - App-open shown immediately after rewarded dismiss —
_lastFullscreenDismissAtwas recorded inside the rewardedonDonecallback which fires on reward-earned (mid-video), not on actual dismiss. Replaced with slot-state watchers that fire onshowing → !showingtransition for all 3 fullscreen slots — authoritative dismiss timestamp regardless of adapter quirks. - VIP grace not auto-expiring mid-session — added
TimerinVipManagerscheduled for the soonestexpiresAt. Fires_purgeExpired+_refreshActivewhen an entry expires, so the SDK reflects VIP loss without requiring a full re-init. Especially relevant for short debug grace windows. - Inter/rewarded/app-open not preloaded after VIP expires mid-session —
added listener on
VipManager.activeListenable; ontrue → falseflip, triggersloadAppOpenAd + loadInterstitial + loadRewardedAd + preloadBannerso the user doesn't see "ad not ready" on first show after losing VIP. canShowFullscreenAd/canShowAppOpenOnResumereported "wait 0s" — sub-second waits truncated to 0. New_fmtWait(ms)helper renders ms when < 1 s ("wait 645ms") and 1 decimal seconds otherwise ("wait 1.5s").- Splash budget warning fired while splash app-open ad still showing —
_armSplashBudgetnow detectsappOpenSlot.isShowingon first elapse and re-arms a 30 s hard cap instead of force-firingmarkSplashInactive. canShowAppOpenOnResumereturnedbool— refactored to returnAdSafetyResult(canShow + reason), aligning withcanShowFullscreenAd. Callers can log the specific block reason + remaining wait time.
Added — Consent flow
ConsentManager— standalone helper class owning the Cupertino consent dialog UI, persistence (SharedPreferences), and provider apply pipeline. Accessible viaAdManager().consentManagerorConsentManager.instance.ConsentSettings— persistent user-choice record withhasBeenAsked,askedAt, JSON serialisation.ConsentDialogStrings— localisation, includesConsentDialogStrings.vifor Vietnamese.- Custom binary Cupertino dialog (Allow / Reject) with hero icon, gradient Allow button, accent colours, scale-in animation, haptic feedback.
AdConfig.autoShowConsentDialog(defaulttrue) — SDK auto-presents the dialog ~1 s aftermarkSplashInactive(post-splash, on home), not during splash flow. Skipped for VIP users.AdConfig.consentDialogPostSplashDelay(default 1 s) — tunable.AdConfig.consentBarrierDismissible(defaultfalse).
Added — UMP (User Messaging Platform) wrapper
requestUmpConsentFlow(testMode, debugGeography, testIdentifiers, …)— wraps Google's built-inConsentInformation+ConsentForm(no extra dependency, available sincegoogle_mobile_ads6.x).AdManager().requestUmpConsent(…)— auto-applies UMP result to providers.- Re-exports
ConsentStatusandDebugGeographyfromgoogle_mobile_adsso callers don't need a direct import.
Added — First-install VIP grace
AdConfig.firstInstallVipGrace: FirstInstallVipGrace(defaultauto= 30 s in debug, 24 h in release) — auto-grants a one-time VIP entry on the very first SDK init for this install. Improves D1 retention by giving the freshly-installed user an ad-free first session.FirstInstallVipGraceclass withauto/disabled/day/debugShortpresets and customDurationconstructor.AdConfig.firstInstallVipKey(default__FIRST_INSTALL__) — VIP entry key for analytics discrimination.AdPreferences— new keys_keyFirstInstallApplied(one-shot guard) and_keyFirstInstallAt(epoch-ms install timestamp for analytics).
Added — Diagnostic logging
🚀 AdManager singleton CREATEDmarker fires once per process on cold start. Two markers in the same logcat session = Android killed and restarted the process.🚨 lifecycle DETACHEDwarning when Flutter engine tears down.- Lifecycle observer logs full state (prev → current, slot states, VIP
flag, splash flag, backgrounded duration) — wrapped in
_safeLifecycleLogso a closure-evaluation throw cannot abort the observer. - All ad-load/show paths emit explicit
⏭️ skipped — <reason>logs for every gate (adapter null, VIP, no network, slot showing, safety reason) instead of returning silently. - AppLovin
onAdDisplayedCallbackextended withnetwork,creativeId,placement,latencyMillisfor revenue diagnosis. - Memory-pressure log throttled to 60 s/event so fast bg/fg cycles don't flood the buffer; payload includes banner state and VIP flag.
DebugAdOverlay— newenabledconstructor flag plus staticgloballyVisibleValueNotifierfor runtime toggle (e.g., from a shake-menu or dev console).
Added — Misc
AdConfig.autoShowConsentDialogskip path also covers the case where the user redeems a VIP key during the 1 s post-splash schedule window (re-checked at fire time).AdManager.processStartedAtMsgetter.
Changed
loadInterstitial/loadRewardedAd/loadAppOpenAd/showInterstitial/showRewardedAd/showAppOpenAdskip-path logs now name the specific reason (adapter null vs VIP vs no network vs already showing vs safety).- Banner preload during VIP active is now skipped at AdManager level — saves a network request and avoids inflating internal impression counter.
Notes for integrators
- Activity manifest: do not set
android:taskAffinity=""on yourMainActivitywhen using AppLovin. With empty affinity, AppLovin'sAppLovinFullscreenActivitylands in a different Android task; after user backgrounds + foregrounds and dismisses the ad, the task may be empty and the user is dropped to launcher. Default affinity (= package name, by simply omitting the attribute) is safe.
- AppLovin App-Open timeout false-positive — old fixed 10 s timeout fired
while user was still interacting with the ad (click → browser → return),
marking dismiss with
-
1.0.1424 Mar 2026Nothing published for this version
-
1.0.1323 Mar 2026Nothing published for this version
-
1.0.1223 Mar 2026Nothing published for this version
-
1.0.1123 Mar 2026Nothing published for this version
-
1.0.1023 Mar 2026Nothing published for this version
-
1.0.923 Mar 2026Nothing published for this version
-
1.0.823 Mar 2026Nothing published for this version
-
1.0.723 Mar 2026Nothing published for this version
-
1.0.623 Mar 2026Nothing published for this version
-
1.0.523 Mar 2026Nothing published for this version
-
1.0.423 Mar 2026Nothing published for this version
-
1.0.323 Mar 2026Nothing published for this version
-
1.0.223 Mar 2026Nothing published for this version