diff --git a/README.md b/README.md index f50cd02..ea61cdf 100644 --- a/README.md +++ b/README.md @@ -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. + +The demo screen after one round: CloudX loaded at 0.0005, AdMob closed, arbiter ADMOB with 2 bids, revenue reported +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. + +```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. + +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 @@ -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 @@ -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 | |---|---| @@ -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 @@ -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() @@ -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. @@ -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. @@ -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: $`, `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 | |---|---|---| @@ -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: @@ -164,28 +206,13 @@ and the `` 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. - -The demo screen after one round: CloudX loaded at 0.0005, AdMob closed, arbiter ADMOB with 2 bids, revenue reported -The AdMob test interstitial shown as the stored arbiter winner - -| Line | Meaning | -|---|---| -| `CloudX` / `AdMob` | That side's last event: `loading`, `loaded: $`, `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.