bevy_ios_toolkit 0.2.0

Native iOS integrations for Bevy as ECS resources and messages: StoreKit IAP, Google AdMob ads, App Tracking Transparency, Game Center, review prompts, haptics and safe-area — each behind a feature, over a polled C-ABI Swift bridge with a testable desktop fake.
Documentation
# bevy_ios_toolkit

Native iOS integrations for [Bevy](https://bevyengine.org), exposed as ordinary
ECS resources and messages. One crate, one plugin, a **feature per integration**:

| feature | module | what it bridges |
|---------|--------|-----------------|
| `storekit` | `store` | StoreKit 2 in-app purchases |
| `ads` | `ads` | Google AdMob ads + UMP (GDPR) consent |
| `att` | `att` | App Tracking Transparency prompt |
| `gamekit` | `gamekit` | Game Center auth, leaderboards, achievements |
| `review` | `review` | StoreKit review prompt |
| `platform` | `platform` | impact haptics, safe-area inset, outbound links |

> **Status: experimental (0.1, pre-release).** APIs will move. Live behaviour
> needs a real device, the relevant Apple/Google setup, and the matching Swift
> shim linked from the companion SPM package — see "iOS integration". Everything
> is fully exercisable on desktop first via the built-in fakes.

## How it works

Every module shares one native contract:

- Each native entry point is `@_cdecl` C-ABI, called **from Rust**.
- The SDKs' async, delegate-driven work surfaces as **polled state** (or a
  drained event queue) read once per frame — *never* callbacks into Rust,
  because re-entrancy against winit's event loop is not safe.
- Each Swift shim (an SPM product, see "iOS integration") sits behind
  `#if canImport(...)` with linking stubs, so the staticlib links on any target.

Off iOS every module is a **stateful, env-tunable fake**, so the whole app flow —
purchases, ads, rewards, consent, ATT, Game Center — runs on `cargo run` desktop
builds with no device.

## Features are opt-in for a reason

No feature is enabled by default. A module's `extern "C"` block only exists when
its feature is on, and the matching SPM product must be linked into your app — so
enabling a feature you haven't wired natively fails loudly at link time instead
of misbehaving at runtime.

```toml
bevy_ios_toolkit = { version = "0.2", features = ["storekit", "ads", "att"] }
```

## Quick start

```rust
use bevy::prelude::*;
use bevy_ios_toolkit::prelude::*;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, IosPlugin))
        .insert_resource(AdmobConfig::test_ads())          // `ads`
        .insert_resource(StoreConfig {                     // `storekit`
            product_ids: vec!["com.example.app.removeads".into()],
        })
        .run();
}

// Show an interstitial once it's loaded.
fn show(inv: Res<AdInventory>, mut shows: MessageWriter<ShowAd>) {
    if inv.is_loaded(AdFormat::Interstitial) {
        shows.write(ShowAd(AdFormat::Interstitial));
    }
}

// Gate features on ownership (covers purchase, restore, relaunch).
fn gate(entitlements: Res<Entitlements>) {
    if entitlements.owns("com.example.app.removeads") { /* hide ads */ }
}
```

See [`demo/`](demo/) for a button-per-feature app that runs on desktop and iOS.

## iOS integration

The Swift shims ship as a Swift package **in this same repo**, co-versioned with
the crate — one git tag pins both halves, which keeps the `@_cdecl` ↔ `extern "C"`
contract in lockstep. You link only the products for the features you ship; there
are no files to vendor or keep in sync by hand.

1. Add this crate with the features you ship.
2. Add this repo as a Swift package dependency and link the matching products.
   Each product links its own system frameworks; `Ads` brings the Google Mobile
   Ads + UMP SDKs transitively. The symbol prefixes (`store_`, `platform_`, `admob_`,
   `att_`, `gamekit_`, `review_`) won't collide with your own bridge.

   | cargo feature | SPM product |
   |---------------|-------------|
   | `platform` | `Platform` |
   | `storekit` | `Store` |
   | `ads` | `Ads` |
   | `att` | `Att` |
   | `gamekit` | `GameCenter` |
   | `review` | `Review` |

3. Per-feature native setup (stays in your app — the package ships none of it):
   - **ads** — set `GADApplicationIdentifier` in `Info.plist` (use `TEST_APP_ID`
     in dev) and add the `SKAdNetworkItems` Google ships.
   - **att** — add `NSUserTrackingUsageDescription` to `Info.plist`.
   - **gamekit** — enable the Game Center capability.
   - **storekit** — define products in App Store Connect (or a StoreKit config).
4. The `demo/ios/` XcodeGen project shows the whole wiring end to end — it
   consumes the package by relative path.

## Testing

```bash
cargo test --features all
cargo run --example store --features storekit
cargo run --example ads   --features ads
```

The fakes are env-tunable (force no-fill, show-failures, consent prompts, ATT
outcomes, Game Center sign-out) — each module documents its knobs. On-device
behaviour must be validated in Xcode with the SDKs linked; this crate can't
simulate Apple's or Google's servers.

## Compatibility

| `bevy_ios_toolkit` | `bevy` | iOS | AdMob SDK |
|--------------------|--------|-----|-----------|
| 0.2                | 0.19   | 16+ | 12.x (+ UMP 2.x) |

## Authorship

Much of this project — the Rust crate, the Swift bridges, its tests, and these
docs — was written by **Claude Opus 4.8** (Anthropic) under human direction and
review. It ships with a passing test suite and a runnable demo, but it's young
(0.x): read the code, run your own tests, and validate the native iOS paths on a
real device before relying on it in production. Bug reports and PRs welcome.

## License

Licensed under either of [Apache-2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT) at
your option.