Repository navigation
feat(monetization): build-out — core expansion, rewarded gates, foss/play split (ADR 0025) - #183
Conversation
…ersist/clamp Build-out of the AdPolicy core per the 2026-06-23 maintainer decisions (all pure logic, host-injected time, determinism boundary intact; 13 -> 17 tests): - PremiumFeature expanded to six: + FastForward, Shaders, Cheats on top of the three persistence locks. This OVERRIDES the earlier "fast-forward stays free" doc stance (maintainer "expand the premium set"); the free tier keeps accuracy/video/audio/ input/pause/rewind but these power tools now need the unlock. - Session-awareness + first-impression protection: begin_session(index, now) + AdConfig.suppress_first_session (no interstitials in session #1) + AdConfig.first_session_play_ms (a generous/ungated first game; default 30 min). - Offline grace: AdConfig.offline_grace_ms + can_grant_offline_grace() / grant_offline_grace() — a one-time per-session continuation so an offline free user at run-out (no rewarded fill) degrades gracefully instead of dead-ending. - In-progress persistence: PlayProgress record + export_progress() / restore_progress() so the timer/cap survive a kill-relaunch, closing the restart-to-reset hole. - Remote-config safety: clamp_ad_config() clamps every field to a sane range so a bad remote push can't brick the gate (host fetches -> overlay on defaults -> clamp -> build). Core stays mobile-only + no_std-irrelevant (host concern); never touches rustynes-core. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he shells - RewardedGate.kt / RewardedGate.swift: the MaxRewardedAd / MARewardedAd gate mirroring AdGate, the sibling that extends the free-tier budget. Grants time ONLY on the reward callback (onUserRewarded / didRewardUser) via grantRewardedTime(); the offer is gated on can_offer_rewarded() and falls back to the Full Version prompt / offline-grace path when no ad is ready. - Clarify the AdConfig::default doc: 8-min regular session, 30-min generous first session (the 8_000/20_000 in the test helpers are seconds, for fast unit tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… set Update the FFI table for the core expansion: begin_session, can/grant_offline_grace, export/restore_progress, clamp_ad_config, the three new AdConfig fields, PlayProgress, and the six-variant PremiumFeature. Flag that FastForward/Shaders/Cheats are now premium (overriding the earlier "fast-forward free" stance) and the 8-min/30-min-first free-tier budget. Note bindings are generated on demand (committed snapshot dropped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Less burdensome free-tier extension: each completed rewarded ad now adds +11 minutes,
capped at 2 grants per session (still +22 min max -> 30 min total on a regular 8-min
session), so a fully-engaged free user watches only TWO ads instead of eleven.
- Core: reward_play_ms 120_000 -> 660_000 (11 min), max_reward_grants_per_session
11 -> 2; clamp ceiling raised to 20 min so 11 fits; default-contract test updated
(8 + 2*11 = 30 min). 17 tests pass.
- Shells (RewardedGate.{kt,swift}) + all docs/monetization swept from "+2 min /
11-grant" to "+11 min / 2-grant".
The offline-grace continuation stays a separate one-time +2 min; the +22-min ceiling
and 30-min total are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…play split (ADR 0025) Per the maintainer's 2026-06-23 decision: capture the foss/play Android flavor split as v2.1.0 architecture, and wire the clean monetization crate into the Android build now. Decision (ADR 0025): split the Android build into a `foss` flavor (default — no Google SDKs, no ads, no tracking; the F-Droid + sideload artifact) and a `play` flavor (all six proprietary SDK groups — Billing, Cast, Play Games, Integrity, update/review + AppLovin + RevenueCat — for Google Play). F-Droid requires a Google-/ad-free build and a BuildConfig flag gates code not packaging, so a product flavor is the only clean-channel path. The five existing proprietary subsystems move behind src/play façades (no-op in src/foss) + an installDebug alias; both flavors verified on-device at v2.0.9; foss -> F-Droid and play -> Google Play at v2.1.0. Captured in v2.0.x-mobile-finalization-plan.md (work items across v2.0.1-v2.0.4), DEFERRED-AND-CARRYOVER, docs/android.md. Wired now (the clean part): android/app/build.gradle.kts builds librustynes_monetization.so via cargo-ndk and generates its UniFFI Kotlin bindings (com.doublegate.rustynes.monetization.ffi) into the main source set — dormant; nothing calls it yet (the AppLovin/RevenueCat glue is the v2.1.0 play-flavor step). The crate is our own AdPolicy core: no Google, no ads, no tracking, so it is flavor-agnostic. Verified: the crate cross-builds for arm64 and the bindgen emits the full AdPolicy surface (incl. the build-out API). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Expands the rustynes-monetization subsystem toward the planned v2.1.0 mobile store launch by updating the freemium time-gate model, adding session/offline/persistence/remote-config hardening to the Rust core, documenting the Android foss/play flavor split (ADR 0025), and wiring the monetization UniFFI bindings into the Android Gradle build (still dormant).
Changes:
- Updated monetization core contract: expanded
PremiumFeature, added first-session behavior, offline grace, progress export/restore, and remote-config clamping. - Added rewarded-ad gate shells for Android (Kotlin) and iOS (Swift) that grant time only on the reward callback.
- Documented the ADR + rollout plan for
foss/playAndroid distribution flavors and updated monetization documentation to the new reward model.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| to-dos/plans/v2.0.x-mobile-finalization-plan.md | Adds the planned foss/play flavor split workstream and sequencing notes. |
| to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md | Adds a deferred item summarizing the flavor split and related wiring/verification. |
| docs/monetization/rustynes-integration.md | Updates monetization integration doc for the new rewarded grant model. |
| docs/monetization/recommendations.md | Updates recommendation copy to reflect +11 min reward wording/cap references. |
| docs/monetization/README.md | Updates monetization doc index/status; still needs consistency fixes (see comments). |
| docs/monetization/pre-implementation-addendum.md | Updates the core model narrative; still needs a few value references aligned (see comments). |
| docs/monetization/platform-setup-runbook.md | Updates AppLovin/MAX setup wording to match +11 min reward grant. |
| docs/monetization/implementation-brief.md | Partially updated; still contains outdated constant values (see comments). |
| docs/monetization/build-and-bindings.md | Updates FFI surface + adds build-out additions section; needed corrections noted. |
| docs/monetization/AGENT-GUIDANCE.md | Updates internal agent guidance; needed corrections noted. |
| docs/android.md | Documents the planned foss/play split and the (dormant) monetization-core wiring. |
| docs/adr/0025-foss-play-android-flavor-split.md | New ADR defining the distribution flavor split decision and mechanics. |
| crates/rustynes-monetization/src/monetization.rs | Core logic expansion: new config fields, session/offline/progress/clamp behavior, tests. |
| crates/rustynes-monetization/shells/ios/RewardedGate.swift | New iOS rewarded-ad gate shell (AppLovin MAX) calling grantRewardedTime() only on reward. |
| crates/rustynes-monetization/shells/android/RewardedGate.kt | New Android rewarded-ad gate shell (AppLovin MAX) calling grantRewardedTime() only on reward. |
| android/app/build.gradle.kts | Builds rustynes-monetization via cargo-ndk and generates Kotlin UniFFI bindings in preBuild. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Code Review
This pull request updates the monetization model for the RustyNES mobile ports, changing the free-tier rewarded ad extension from +2 minutes (capped at 11 grants) to +11 minutes (capped at 2 grants) per session. It also introduces a generous first-session budget, first-session interstitial suppression, a one-time offline grace budget, progress persistence, and expands the premium feature set to include fast-forward, shaders, and cheats. Additionally, it integrates the monetization crate into the Android build, adds Android and iOS rewarded ad gates, and documents a planned foss/play flavor split. The reviewer feedback suggests clamping the minimum first-session play budget to 1 minute in clamp_ad_config to prevent potential app bricking from a bad remote config, and highlights several leftover references to the old 11-grant/2-minute model in the documentation.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…doc sweep - clamp_ad_config: floor first_session_play_ms at 60_000 (Gemini HIGH) — a remote `0` would set session #1's budget to 0 ms and brick the app for new users; +1 test. - Reconcile the reward-model doc stragglers my earlier sweep missed (and one over-reach): the +11 min × 2-ad model now consistent everywhere (cap 11 -> 2, reward_play_ms 120_000 -> 660_000, the §9 tuning table, the "reward 2 min" open-decision lines), the offline-grace over-reach reverted to +2 min, the unit-test count 13 -> 17, and the "fast-forward stays free" lines updated to the six-feature premium set (FastForward / Shaders / Cheats now premium) across README / rustynes-integration / implementation-brief / shells README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Monetization build-out (v1.8.9)
Builds the harvested
rustynes-monetizationscaffold out toward final, per the2026-06-23 maintainer decisions. Dormant until the v2.1.0 joint store launch.
Core (
crates/rustynes-monetization) — 17 testsPremiumFeature→ six variants — added FastForward / Shaders / Cheats to thethree persistence locks ("expand the premium set"; overrides the old "fast-forward free"
doc stance).
begin_session, 30-min generous first session(
first_session_play_ms), interstitials suppressed in session chore(ci): Bump actions/cache from 4 to 5 #1.export/restore_progress(survives kill-relaunch).clamp_ad_config(a bad push can't brick the gate).far less burdensome (two ad watches, not eleven).
Shells
RewardedGate.{kt,swift}— the free-tier rewarded-ad gate (grants only on the rewardcallback). Earlier PR feat(monetization): integrate the rustynes-monetization scaffold #182 added the PLAY_BUILD gating + LIFETIME-package + deep-sleep clock.
Android —
foss/playflavor split locked + crate wired (ADR 0025)docs/android.md: the v2.1.0 build splits into afossflavor (no Google SDKs, no ads, no tracking — the F-Droid + sideload artifact) and a
playflavor (all proprietary SDKs + AppLovin + RevenueCat — Google Play). F-Droidrequires a Google-/ad-free build, so the flavor is the only clean-channel path.
build.gradle.ktsbuildslibrustynes_monetization.soviacargo-ndk and generates its UniFFI Kotlin bindings — our own clean
AdPolicycore, noGoogle/ads, so flavor-agnostic. The full split (5 existing SDKs behind
src/playfaçades +the
installDebugalias + both-flavor on-device verify) is a v2.1.0 deliverable.Docs
All
docs/monetization/reconciled (FFI table, the expanded premium set, the +11×2 rewardmodel, the in-repo locations).
Core stays host-side only — never touches
rustynes-core; AccuracyCoin/determinism untouched.🤖 Generated with Claude Code