Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 90 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
# CloudX Trusted Arbiter Demo - Flutter

A working Trusted Arbiter integration for interstitials. CloudX and AdMob load
in parallel, both fills become bids, and `CloudX.arbiter` decides which one is
shown. Companion to [docs.cloudx.io](https://docs.cloudx.io/en/flutter).
A complete, runnable Flutter app showing how to put CloudX in competition with
another ad network on price. CloudX and AdMob load an interstitial in parallel,
both fills become bids, and `CloudX.arbiter` decides which one gets shown.

Rewarded ads follow the same flow with one extra callback. Banners are a
different shape entirely: `CloudX.createBanner` places a view at a fixed screen
position and refreshes it on its own, with no arbiter round. Neither is
repeated here; interstitial is the whole demo.
Clone it and run it. It works out of the box against CloudX's public sample app,
so you can watch a real auction before you change a single id.

<img src="docs/images/arbiter-screen.png" width="260" alt="The demo screen after one round: CloudX loaded at 0.0005, AdMob closed, arbiter ADMOB with 2 bids, revenue reported">
<img src="docs/images/interstitial.png" width="260" alt="The AdMob test interstitial shown as the stored arbiter winner">

Interstitials only. Rewarded ads follow the same flow with one extra callback,
and banners are a different shape entirely (`CloudX.createBanner` places a view
at a fixed position and refreshes it on its own, with no arbiter round), so
neither is repeated here.

Full documentation: [Flutter integration
guide](https://docs.cloudx.io/en/flutter/integration) and [Trusted
Arbiter](https://docs.cloudx.io/en/flutter/trusted-arbiter).

## Run it

You need Flutter 3.44 or newer (Dart 3.12), and Xcode if you want the iOS side.
Nothing else: the ids checked in belong to CloudX's public sample app, so the
demo runs as-is.
Comment on lines +24 to +26

```sh
flutter pub get
(cd ios && pod install) # iOS only
flutter run
```

Tap **Load both**, wait for both sides to settle, then tap **Show winner**. On
iOS you will be asked for tracking permission first; see the ATT note under
[Point it at your own app](#point-it-at-your-own-app) for why that has to come
before anything else.
Comment on lines +34 to +37

If you want the iOS build on a device or an archive, set your own Signing Team
in Xcode. The project deliberately ships with no `DEVELOPMENT_TEAM`, so signing
stays on automatic and resolves to your team rather than ours.

## What Trusted Arbiter is

Expand All @@ -16,7 +47,7 @@ demand compete against CloudX on price instead of sitting in a waterfall above
or below it: you load both, hand both to CloudX as bids, and CloudX tells you
which one to show.

Two rules are easy to miss, and both are in the code:
Two rules are easy to miss, and both are visible in the code:

> **The arbiter runs before the show, not during it.** Both sides load, the
> arbiter picks a winner, and the winner is stored. The tap that shows an ad
Expand All @@ -34,11 +65,11 @@ AdMob bid therefore reaches the arbiter with no price at all. Expect CloudX to
lose those rounds. Point the demo at a real AdMob unit that pays to see prices
compete.

## What is in here
## What to copy into your app

Copy [`lib/cloudx/`](lib/cloudx). Those six files are the whole integration,
and none of them builds a widget. Everything outside that folder is this demo's
own scaffolding.
Copy [`lib/cloudx/`](lib/cloudx). Those six files are the whole integration, and
none of them builds a widget, so they drop into an app with any UI. Everything
outside that folder is this demo's own scaffolding.

| File | What it is |
|---|---|
Expand All @@ -48,10 +79,11 @@ own scaffolding.
| [`lib/cloudx/tracking_gate.dart`](lib/cloudx/tracking_gate.dart) | The iOS App Tracking Transparency gate. |
| [`lib/cloudx/cloudx_failure_text.dart`](lib/cloudx/cloudx_failure_text.dart) | One line out of a CloudX failure, carrying the SDK's own name for the error code. |
| [`lib/cloudx/demo_config.dart`](lib/cloudx/demo_config.dart) | App key and ad unit ids, per platform. The first file to edit. |
| [`lib/ui/arbiter_screen.dart`](lib/ui/arbiter_screen.dart) | Demo-only UI. Ignore it when reading the integration. |
| [`lib/main.dart`](lib/main.dart) | `runApp`, nothing else. |

**Take fewer and it will not build.** The controller reports through
`lib/ui/` and `lib/main.dart` are the demo's screen and entry point. They are
here so the app runs; they are not part of the integration.

**Take fewer files and it will not build.** The controller reports through
`arbiter_events.dart`, both it and `sdk_startup.dart` format failures through
`cloudx_failure_text.dart`, and `sdk_startup.dart` calls `tracking_gate.dart`.
If your app already initializes CloudX and answers the ATT prompt, drop
Expand All @@ -66,14 +98,7 @@ Read them in this order:
3. [`arbiter_interstitial_controller.dart`](lib/cloudx/arbiter_interstitial_controller.dart)
- the cycle itself

**No CloudX or AdMob call lives outside `lib/cloudx/`.** That is checkable:
`grep -rn "CloudX\.\|MobileAds\." lib/ui lib/main.dart` returns only two
strings printed on screen. The integration files import
`package:flutter/foundation.dart` for logging, and `tracking_gate.dart` also
needs `package:flutter/widgets.dart` to wait for the app to become active before
prompting, but none of them imports `material.dart` or builds a widget.

## The cycle
## How the cycle works

```
load()
Expand All @@ -89,7 +114,8 @@ prompting, but none of them imports `material.dart` or builds a widget.
ad closes --> that side's fill is consumed --> load() reloads only what is missing
```

Details worth knowing:
Four things in that cycle are easy to get wrong. The code handles all four, and
they are worth understanding before you adapt it:

- **A `none` result is not a winner.** It is stored as "nothing", so the next
`load()` runs the round again instead of parking on a winner that cannot show.
Expand All @@ -104,15 +130,21 @@ Details worth knowing:
`LOAD_NOT_ALLOWED_WHILE_SHOWING`. Destroying leaves the next load to build a
fresh instance, which is also what makes it run a new auction.

## Required setup
## Point it at your own app

1. **Enable Trusted Arbiter for your app in the CloudX dashboard.** No code here
can turn it on. You can confirm it from the logs at startup:
The ad unit ids checked in here belong to CloudX's public sample app
(`io.cloudx.sample`), and the AdMob ids are Google's public test units. Replace
all of them, and do these five things together:

1. **Ask CloudX to enable Trusted Arbiter for your app.** No code here can turn
it on. Confirm it from the logs at startup:
`[InitializationService] Arbiter enabled: https://sdk.cloudx.io/arbitration`.
If that line is missing, the arbiter call will not do what this demo shows.
2. **Match your app key to your bundle id.** Bid requests are authorized per app
key AND bundle id. Change `lib/cloudx/demo_config.dart`, the Android
`applicationId` and the iOS `PRODUCT_BUNDLE_IDENTIFIER` together, or every
round comes back `NO_FILL[302]` with nothing to say the pairing is why.
`applicationId` and the iOS `PRODUCT_BUNDLE_IDENTIFIER` together. Get this
wrong and every round comes back `NO_FILL[302]`, with nothing on screen to
say the pairing is the reason.
3. **Set the Google Mobile Ads application id natively**, in
`android/app/src/main/AndroidManifest.xml` and `ios/Runner/Info.plist`. The
Google SDK throws at startup when it is absent.
Expand All @@ -125,15 +157,25 @@ Details worth knowing:
The prompt is answered while `CloudX SDK` still reads `not initialized`. That
order is the point: initialize first and every request that session goes out
without an IDFA and with `dnt = 1`.
5. **Set your Signing Team in Xcode** before an iOS device or archive build. The
project ships with no `DEVELOPMENT_TEAM` on purpose, so signing stays on
automatic and resolves to your own team.
5. **Set your Signing Team in Xcode** before an iOS device or archive build, as
above.

## Reading the screen

Every status line names the platform it came from, so the screen doubles as the
diagnostic.

| Line | Meaning |
|---|---|
| `CloudX` / `AdMob` | That side's last event: `loading`, `loaded: <network> $<price>`, `load failed: ...`, `showing`, `closed`. A CloudX failure carries the SDK's own name for the code, so a round that did not fill reads `load failed: No ad available. (NO_FILL[302])` rather than a bare number. |
| `Arbiter` | `ADMOB (2 bids)`, `CLOUDX (2 bids)`, `no winner (1 bid)`, or `failed: ...`. |
| `Revenue -> CloudX` | The last AdMob paid event forwarded through `reportRevenueData`, and what that call returned. A `true` does not mean the price was kept; a revenue of 0.0 is discarded. |

> The ad unit ids checked in here belong to the public CloudX sample app
> (`io.cloudx.sample`), and the AdMob ids are Google's public test units.
> Replace all of them.
If `Arbiter` only ever reads `(1 bid)`, one side is not filling. Look at which
of the two lines above it says `load failed`; the arbiter is working correctly
either way.

## Versions
## Versions and adapters

| Pin | Version | Why |
|---|---|---|
Expand All @@ -146,16 +188,16 @@ Details worth knowing:
Dart 3.12 and Flutter 3.44 are the floors, set by `webview_flutter_android` and
`webview_flutter_wkwebview`, which `google_mobile_ads` 9.1.0 pulls in.

The adapter list in `android/app/build.gradle.kts` and `ios/Podfile` is the full
CloudX set, so you can see the shape of it. **Take only the networks your
dashboard actually serves**; each one adds to your binary.

**`CloudXGoogleWaterfallAdapter` / `io.cloudx:adapter-googlewaterfall` is
deliberately absent.** It runs AdMob demand *inside* the CloudX auction, which
is the opposite of what this demo shows: here AdMob is an external bid competing
against CloudX through the arbiter. Shipping both would make the two bids the
same demand.

The adapter list in `android/app/build.gradle.kts` and `ios/Podfile` is the full
CloudX set. Take only the networks your dashboard actually serves; each one adds
to your binary.

**BIGO is Android only**, which is why the Gradle file lists one network more
than the Podfile. It also needs cleartext traffic to `127.0.0.1`, because the
BIGO Ads SDK serves some creative assets from a loopback server on the device:
Expand All @@ -164,28 +206,13 @@ and the `<application>` element references it. Drop the adapter and you can drop
both. See the
[BIGO adapter page](https://docs.cloudx.io/en/android/adapters/bigo/overview).

## Running
## Getting help

```sh
flutter pub get
(cd ios && pod install) # iOS only
flutter run
```
Start with the [Flutter integration
guide](https://docs.cloudx.io/en/flutter/integration), the [Trusted Arbiter
page](https://docs.cloudx.io/en/flutter/trusted-arbiter), and the
[changelog](https://docs.cloudx.io/en/flutter/changelog). The plugin itself is
on pub.dev as [`cloudx_flutter`](https://pub.dev/packages/cloudx_flutter).

## Reading the screen

Every status line names the platform it came from, so the screen doubles as the
diagnostic.

<img src="docs/images/arbiter-screen.png" width="260" alt="The demo screen after one round: CloudX loaded at 0.0005, AdMob closed, arbiter ADMOB with 2 bids, revenue reported">
<img src="docs/images/interstitial.png" width="260" alt="The AdMob test interstitial shown as the stored arbiter winner">

| Line | Meaning |
|---|---|
| `CloudX` / `AdMob` | That side's last event: `loading`, `loaded: <network> $<price>`, `load failed: ...`, `showing`, `closed`. A CloudX failure carries the SDK's own name for the code, so a round that did not fill reads `load failed: No ad available. (NO_FILL[302])` rather than a bare number. |
| `Arbiter` | `ADMOB (2 bids)`, `CLOUDX (2 bids)`, `no winner (1 bid)`, or `failed: ...`. |
| `Revenue -> CloudX` | The last AdMob paid event forwarded through `reportRevenueData`, and what that call returned. A `true` does not mean the price was kept; a revenue of 0.0 is discarded. |

If `Arbiter` only ever reads `(1 bid)`, one side is not filling. Look at which
of the two lines above it says `load failed`; the arbiter is working correctly
either way.
For an app key, ad unit ids, or to have Trusted Arbiter switched on, talk to
your CloudX contact.