Skip to content

Bootstrap the public demo: Trusted Arbiter for interstitials - #1

Merged
antonurankar-moloco merged 19 commits into
mainfrom
tomi-demo-app-bootstrap
Sep 13, 2026
Merged

antonurankar-moloco merged 19 commits into
mainfrom
tomi-demo-app-bootstrap

Conversation

@antonurankar-moloco

@antonurankar-moloco antonurankar-moloco commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The first contents of this repository: a single-screen Trusted Arbiter demo for interstitials, with CloudX and AdMob loading in parallel, both fills becoming bids, CloudX.arbiter picking the winner, and the winner shown from a stored result so the show path makes no network call.

Interstitials only. Rewarded is the same flow plus one reward callback, and banners are a different shape entirely, so neither is repeated here.

78 files, of which most are the Flutter and Xcode scaffolding flutter create produced. The demo itself is seven Dart files.

What a reader is meant to copy

Everything under lib/cloudx/. None of those files builds a widget, and that is checkable: grep -rn "CloudX\.\|MobileAds\." lib/ui lib/main.dart returns only two strings printed on screen.

File What it is
lib/cloudx/arbiter_interstitial_controller.dart The integration. The whole load, arbitrate, show cycle and both SDKs' calls, in one file.
lib/cloudx/arbiter_events.dart The callbacks the controller reports through.
lib/cloudx/sdk_startup.dart Brings both SDKs up, in the order they have to come up in.
lib/cloudx/tracking_gate.dart The iOS App Tracking Transparency gate.
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 App key and ad unit ids, per platform. The first file to edit.
lib/ui/arbiter_screen.dart Demo-only UI. Explicitly not for copying.

Decisions worth reviewing

  • Google Waterfall is deliberately excluded on both platforms. 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. Shipping both would make the two bids the same demand. Leaving it out also frees google_mobile_ads from matching that adapter's exact Google Mobile Ads pin, which is why this app runs google_mobile_ads 9.1.0 while the internal demo is held at 7.0.0.
  • BIGO is Android only. io.cloudx:adapter-bigo:6.0.1.0 is in android/app/build.gradle.kts and has no iOS counterpart, so 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; that is android/app/src/main/res/xml/network_security_config.xml and the android:networkSecurityConfig attribute on <application>.
  • io.cloudx.sample identity, matching the public Unity and React Native demos, so all three public demos are one dashboard app.
  • No forced-no-fill switch. The Unity demo ships ArbiterConfig.ForceCloudXNoFill; this one uses real units only.
  • The AdMob ids are Google's public test units, which report a revenue of 0.0. The price store discards 0.0, so the AdMob bid reaches the arbiter with no price and CloudX loses those rounds. That is called out in the README; point the demo at a paying unit to see prices compete.

Failure lines carry the error code name

Since cloudx_flutter 3.9.0 both CloudXError.codeName and CloudXInitializationResult.errorCodeName carry the native SDK's own name for the code, read from CloudXErrorCode.name on Android and +[CLXError nameForCode:] on iOS. cloudx_failure_text.dart formats both paths so they cannot drift: a round that did not fill reads No ad available. (NO_FILL[302]), and a rejected app key reads ... (NETWORK_CLIENT_ERROR[103]) rather than a bare number. The message is trimmed, because the Android initialize failure arrives with a trailing newline that otherwise pushed the code onto a line of its own.

The dev-setting commit is resolved

Earlier revisions of this branch resolved cloudx_flutter from a local pub host, because 3.9.0 was not published and the standing decision was that it never would be. Both of those have changed: cloudx_flutter 3.9.0 went to pub.dev on 2026-09-13, and commit 9917bd5 puts the manifest back to a plain cloudx_flutter: ^3.9.0 with pubspec.lock recording https://pub.dev and the sha256 of the published archive. Anyone can flutter pub get this branch now, and preflight-hermetic.sh --release passes rather than failing by design.

It was not undone with git revert 833a3e6. That revert conflicts, because pubspec.lock was rewritten twice after it, and its parent also carries the older ^3.8.0 pin and the pre-review Dart and Flutter floors that the first review round corrected. The manifest was edited and the lock regenerated, which is the path a7de163 documented inline.

What the loop review changed

Three rounds of Copilot plus a line-by-line pass and a loop check.

Commit What
a7de163 The documented undo path for the dev setting did not work: git revert 833a3e6 conflicts on both files, and its parent carries ^3.8.0 and the pre-review SDK floors, so taking it would undo an accepted fix. Also stopped the README calling banners an "inline" flow, which is the SDK's word for the now-unexported CloudXAdView.
6f92fe1 Two cues from the public Unity demo: README screenshots out of docs/images/ beside the sections that explain them, and the iOS signing note moved from Versions into Required setup with the app key and bundle id, which is how that demo groups the three things a publisher changes.
bee0111 cloudx_failure_text.dart offered INVALID_APP_KEY[203] as its worked example while this PR's own validation records NETWORK_CLIENT_ERROR[103] for a rejected key. Now quotes codes this demo actually produces. Also records why tracking_gate.dart's check-then-listen is not the race two readers took it for.
9917bd5 Resolve cloudx_flutter from pub.dev now that 3.9.0 is published.
7cdf0e2 flutter build apk --release could not build at all: the BIGO network security config declared 127.0.0.1 with no includeSubdomains attribute, which Android lint reports as an error under lintVital, and that task runs on every release assemble. The attribute changes no behaviour (a literal address has no subdomains, and false is the default); lint only requires the choice to be explicit. The form now matches android/adapter-bigo/README.md in the monorepo, which has always carried it.

Left open for a human: the tracking_gate.dart thread, where the reviewer's race does not exist (no await between the read and the registration, and AppLifecycleListener fires onResume unconditionally on the transition to resumed) but the invariant was worth writing down.

One flag from comparing against the sibling public demos: both the public Unity and React Native demos ship First Look while this one ships Trusted Arbiter, and Flutter is the only one of five platforms with no integrations/first-look.mdx. The earlier flag about https://docs.cloudx.io/en/flutter/trusted-arbiter returning 404 is closed: cloudx-io/docs#434 merged on 2026-09-13 and that page is live.

Validation

Both platforms were run, not just built.

  • flutter analyze — no issues
  • Android, emulator API 36 and a physical Pixel 10 Pro: SDK initialized, Arbiter enabled: https://sdk.cloudx.io/arbitration in the logs, both sides filling (CloudX testbidder $0.0005, AdMob reservation campaign), arbiter: 2 bid(s) then platform=ADMOB, the AdMob interstitial displayed and closed, reportRevenueData(0.0 micros USD, PrecisionType.unknown) returned=true. On the Pixel the BIGO adapter is discovered at startup: network=Bigo Ads, adapterVersion=6.0.1.0, minimumSdkVersion=4.7.0.
  • iOS, simulator iPhone 17: ATT answered before init, same full cycle, same result.
  • Forced-failure runs on both platforms, with the ad unit and then the app key deliberately broken, confirming the named codes reach the screen: INVALID_AD_UNIT[300] on both, NETWORK_CLIENT_ERROR[103] for the Android initialize path. Those edits were reverted; git diff lib/cloudx/demo_config.dart is empty.
  • qa/sdk/scripts/flutter/preflight-hermetic.sh --app <this repo> --release — exit 0, resolved hosted from https://pub.dev at 3.9.0, no path dependency into the monorepo.

Re-verified against the published package

After the switch to pub.dev, the whole thing was run again from a clean room: a fresh clone of this branch resolved with an empty PUB_CACHE, which created only a pub.dev entry and produced a byte-identical pubspec.lock. The published lib/, android/ and ios/ are identical to the flutter/sdk/3.9.0 tag by diff -r, the package's own pins are CloudXCore ~> 3.9.0 and io.cloudx:sdk:4.7.0, and ios/Podfile.lock was unchanged by pod install. One full arbiter round ran on an Android emulator and an iOS simulator booted for the run: two bids, ADMOB won, the interstitial shown and closed, reportRevenueData ... returned=true on both.

  • flutter build apk --debug — built; the APK carries the BIGO resources and adapter classes, and res/xml/network_security_config.xml survives manifest merging.
  • flutter build apk --release — failed before 7cdf0e2 on the lint error above, and now produces app-release.apk at 68.5MB.

antonurankar-moloco and others added 2 commits September 11, 2026 12:00
CloudX and AdMob load in parallel, both fills become bids, and
CloudX.arbiter picks which one is shown. The winner is prepared ahead of
the show, so the tap that shows an ad makes no network call.

The integration is three files: arbiter_interstitial_controller.dart
holds the whole cycle and both SDKs' calls, arbiter_events.dart is what
it reports through, and demo_config.dart carries the ids. lib/main.dart
is the demo screen and is not part of the integration.

Only interstitials. Rewarded is the same flow with one extra callback and
banners are a different, inline flow, so neither is repeated here.

CloudXGoogleWaterfallAdapter and io.cloudx:adapter-googlewaterfall are
deliberately left out: they run AdMob demand inside the CloudX auction,
which is the opposite of what this demo shows, and shipping them would
make the two bids the same demand.

Identity is the public io.cloudx.sample dashboard app, matching the
public Unity and React Native demos. AdMob ids are Google's test units.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
cloudx_flutter 3.8.0 is not on pub.dev yet, so `flutter pub get` cannot
resolve the dependency the previous commit declares. Until the publish
lands, the demo points at the local pub host that
flutter/scripts/serve-local-pub.py in cloudx-io/mobile serves, and
pubspec.lock records that host.

Revert this commit once cloudx_flutter is published. Nothing else depends
on it. qa/sdk/scripts/flutter/preflight-hermetic.sh --release fails while
it is applied, which is the intended behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI lite review requested due to automatic review settings September 11, 2026 10:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical dependency, SDK constraint, signing, runtime, and platform metadata findings block approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a public Flutter demo integrating CloudX Trusted Arbiter with parallel CloudX/AdMob interstitial loading, winner selection, stored-result presentation, ATT gating, and native platform setup.

Changes:

  • Adds arbiter controller, callbacks, configuration, tracking gate, and demo UI.
  • Adds Android/iOS SDK, adapter, AdMob, ATT, and SKAdNetwork configuration.
  • Adds documentation, dependencies, and Flutter project scaffolding.

Unresolved findings:

  • Critical: pubspec.yaml uses a local-only dependency source (3 votes); SDK constraints conflict with the lockfile (3 votes); iOS signing hard-codes a team (1 vote).
  • Moderate: Missing Google SKAdNetwork identifier (2 votes); controller show and in-flight state failures (2 and 1 votes); busy/load-button state issues (2 and 1 votes).
  • Nit: README’s AdMob price example does not match the implementation (1 vote).
File summaries
File Reviewed scope
README.md Demo documentation
pubspec.yaml Dependencies and SDK constraints
pubspec.lock Locked dependency graph
lib/tracking_gate.dart iOS ATT gate
lib/main.dart Demo screen and lifecycle
lib/config/demo_config.dart Platform ad configuration
lib/arbiter/arbiter_interstitial_controller.dart Arbiter load, show, and revenue flow
lib/arbiter/arbiter_events.dart Controller callbacks
ios/RunnerTests/RunnerTests.swift iOS test scaffold
ios/Runner/SceneDelegate.swift iOS scene delegate
ios/Runner/Runner-Bridging-Header.h Swift bridging setup
ios/Runner/Info.plist AdMob, ATT, and SKAdNetwork metadata
ios/Runner/Base.lproj/Main.storyboard Main storyboard
ios/Runner/Base.lproj/LaunchScreen.storyboard Launch screen
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md Launch asset guidance
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json Launch asset metadata
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json App icon metadata
ios/Runner/AppDelegate.swift iOS app delegate
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings Workspace settings
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved Swift package pins
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist Xcode workspace checks
ios/Runner.xcworkspace/contents.xcworkspacedata Workspace references
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme Xcode scheme
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings Project workspace settings
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved Swift package pins
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist Xcode checks
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata Project workspace
ios/Runner.xcodeproj/project.pbxproj iOS project configuration
ios/Podfile.lock Locked CocoaPods
ios/Podfile iOS pods and adapters
ios/Flutter/Release.xcconfig Release Flutter settings
ios/Flutter/Debug.xcconfig Debug Flutter settings
ios/Flutter/AppFrameworkInfo.plist Flutter framework metadata
ios/.gitignore iOS ignore rules
android/settings.gradle.kts Android Gradle plugins
android/gradle/wrapper/gradle-wrapper.properties Gradle version
android/gradle.properties Android build properties
android/build.gradle.kts Android repositories and build setup
android/app/src/profile/AndroidManifest.xml Profile manifest
android/app/src/main/res/values/styles.xml Light launch themes
android/app/src/main/res/values-night/styles.xml Dark launch themes
android/app/src/main/res/drawable/launch_background.xml Android launch background
android/app/src/main/res/drawable-v21/launch_background.xml Android API 21 launch background
android/app/src/main/kotlin/io/cloudx/cloudx_flutter_public_demo/MainActivity.kt Android entry point
android/app/src/main/AndroidManifest.xml Android permissions and metadata
android/app/src/debug/AndroidManifest.xml Debug manifest
android/app/build.gradle.kts Android dependencies and adapters
android/.gitignore Android ignore rules
analysis_options.yaml Dart analyzer configuration
.metadata Flutter project metadata
.gitignore Repository ignore rules
Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (4)

README.md:128

  • This shared example says both lines display loaded: <network> $<price>, but the AdMob callback in main.dart:337 reports only _adMobNetworkName; AdMob intentionally has no bid price. Split the examples or explicitly limit the price suffix to CloudX so the screen guide matches the implementation.
| `CloudX` / `AdMob` | That side's last event: `loading`, `loaded: <network> $<price>`, `load failed: ...`, `showing`, `closed`. |

lib/arbiter/arbiter_interstitial_controller.dart:160

  • The AdMob show call can fail through its awaited platform Future (for example when the stored interstitial has expired). With no catch, _nextWinner has already been cleared but _isShowing remains true, so _show() gets an uncaught error and the controller cannot reload. Catch this failure, dispose/reset the AdMob state, and return false (or route it through the existing show-failed event).
    lib/arbiter/arbiter_interstitial_controller.dart:74
  • The screen uses isBusy to enable Load both, but this getter excludes _isShowing even though load() rejects calls while an ad is visible. Pressing the button during the show therefore overwrites the status lines without starting a load. Include _isShowing in the busy state or gate the button on isShowing.
    lib/main.dart:162
  • load() intentionally starts only the missing side, but this unconditional update marks both sides as loading. After a one-sided failure, the retained ad emits no new loaded callback, so its status remains loading even while it is still held as a bid/winner and the diagnostic screen becomes misleading. Update only the side(s) actually being requested or expose per-side loading state.
  • Files reviewed: 46/74 changed files
  • Comments generated: 6
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ios/Runner.xcodeproj/project.pbxproj Outdated
Comment thread pubspec.yaml Outdated
Comment thread pubspec.yaml Outdated
Comment thread ios/Runner/Info.plist
Comment thread lib/arbiter/arbiter_interstitial_controller.dart Outdated
Comment thread lib/main.dart Outdated
demo_config.dart carries the AdMob ad unit ids, which makes it a
reasonable place to look for the application id too. It is not there and
cannot be: Google reads it before any Dart runs, so it is the
com.google.android.gms.ads.APPLICATION_ID meta-data in AndroidManifest.xml
and GADApplicationIdentifier in Info.plist, and the Google Mobile Ads SDK
throws at startup without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 11, 2026 11:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved build, dependency, and interstitial state-management issues remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (5)

lib/arbiter/arbiter_interstitial_controller.dart:130

  • show() leaves _isShowing false while this readiness call is awaited, so the Load button/API can start another _maybePrepareWinner() round against the same fills. That second result can overwrite _nextWinner; if it selects CloudX, it refers to the ad whose first show is still being prepared or consumed. Mark the show as in-flight before awaiting readiness (and clear it on the stale/failure paths), or otherwise serialize loading/arbitration with this check.
    lib/arbiter/arbiter_interstitial_controller.dart:74
  • isBusy omits _isShowing, so after a show has started the screen's Load both button remains enabled. Tapping it rewrites the UI to loading/waiting for both sides to settle, while load() immediately returns because an ad is showing, leaving misleading status until another cycle. Include the showing state in the busy predicate.
    lib/main.dart:162
  • This unconditionally reports both networks as loading, but load() deliberately reloads only missing fills. After a CloudX win/close, for example, the retained AdMob fill is not requested again and emits no new loaded event, so the demo can permanently display AdMob: loading while that bid is included in the next arbitration. Update only the sides actually being loaded, or expose the controller's per-side state to the screen.
    pubspec.yaml:10
  • The package advertises support for Flutter 3.38.1/Dart 3.10, but the committed resolution requires Flutter >=3.44.0 and Dart >=3.12.0. A user on the advertised minimum cannot resolve this lockfile, so either raise these environment constraints to the resolved floors or regenerate dependencies that meet the advertised range.
    pubspec.yaml:23
  • As committed, a fresh clone following the README cannot run flutter pub get: the direct SDK dependency is hard-wired to an HTTP pub server on 127.0.0.1:8080, and the lockfile records that same local source. This is explicitly a temporary development setting, but it makes the public demo unusable until the SDK is published; please keep this setting out of the mergeable public-demo commit (or otherwise provide a resolvable published dependency).
  • Files reviewed: 46/74 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread lib/cloudx/arbiter_interstitial_controller.dart
Five findings, all verified against the code before fixing.

Signing: the Xcode project hard-coded CloudX's DEVELOPMENT_TEAM in all three
configurations. Removed, so signing stays on automatic and picks the cloner's
own team. The public Unity and React Native demos both ship without a team id;
this project was the only one leaking it.

SDK floors: pubspec.lock needs Dart 3.12 and Flutter 3.44, set by
webview_flutter_android 4.14.1 and webview_flutter_wkwebview 3.26.1 under
google_mobile_ads 9.1.0. The manifest declared 3.10/3.38.1, so an environment it
allows could not resolve the committed lock. Raised to match.

SKAdNetwork: added Google's cstr6suwn9.skadnetwork. The list held the CloudX
adapters only, so Google demand could not attribute on iOS.

show() race: _nextWinner was cleared before the isInterstitialReady round trip
but _isShowing only after it, so a second tap inside that await could
re-arbitrate the fills the first call was about to consume. The show slot is now
claimed before the await and released on every path that does not reach a show.

Load both: the button stayed enabled with a winner stored or an ad showing, both
cases where load() does nothing, so a tap set the rows to "loading" with no
callback coming to clear them. Disabled in both.

Also corrected what the demo claims about reportRevenueData. Its return value is
the ILRD emission result when ILRD telemetry is on, not an acceptance, and the
price store drops any revenue of 0.0 - which is what Google's test units pay, so
the AdMob bid reaches the arbiter unpriced. The row, the log line, the callback
doc and the README now say that instead of "accepted".

Copilot's seventh finding is wrong: google_mobile_ads 9.1.0 declares
OnPaidEventCallback with a double valueMicros (ad_listeners.dart:36-42), which
is what the helper takes.

Verified on an emulator and a simulator booted for this run: two bids, ADMOB
winner, one interstitial per double tap, Load both greyed while a winner is
held, and "0.000000 USD reported (returned true)" on both platforms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 11, 2026 12:27
Claiming _isShowing before the isInterstitialReady round trip closed the
re-arbitration race, but it opened a worse one: a platform-channel failure
throws out of that call, and with the slot already claimed nothing was left to
clear it. load() returns early while _isShowing is set, so the controller wedged
with both buttons dead and no way back short of restarting the app. The throw
also escaped into the button handler unhandled.

Both awaits in show() now catch, release the slot and report a show failure
through the same callback the SDK uses, matching how the arbiter call already
handles a channel failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One or more issues must be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (2)

lib/main.dart:175

  • show() returns false both for a stale winner and when another show is already in progress. This branch treats the latter as stale and calls _load(), which immediately no-ops because controller.isShowing is true, while the UI is left saying it is reloading. Check controller.isShowing before taking the stale-winner recovery path.
    lib/main.dart:166
  • controller.load() retains the losing ad and reloads only the missing side, so after a CloudX win the next load does not emit an AdMob load callback (and vice versa). Resetting both rows to loading leaves the retained side permanently misreported as loading, which makes this diagnostic screen incorrect. Update only the side that is actually being loaded or expose/use controller state for the held fills.
  • Files reviewed: 46/74 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread android/app/build.gradle.kts
Copilot AI review requested due to automatic review settings September 11, 2026 12:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved moderate issues remain in lib/main.dart, along with a README correctness nit.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (4)

Previously missed (2) — in code that hasn't changed since the last review.

lib/main.dart:166

  • load() intentionally starts only the missing side, and after a none result it may only rerun arbitration while both fills are still held. Resetting both rows to loading here therefore leaves the held side permanently showing loading because it emits no load callback (and a winnerless retry may emit only an arbiter event). Update only the side(s) actually being loaded, or expose that state from the controller, so the diagnostic rows stay accurate.
    lib/main.dart:175
  • A second tap while the first show() is awaiting readiness makes show() return false because controller.isShowing is still true. This path treats that normal in-flight rejection as a stale winner, changes the rows to loading, and calls load() while the controller is still showing, so load() is a no-op and the UI remains stuck. Check controller.isShowing before entering the stale-winner reload path (or disable Show while it is in flight).

README.md:34

  • This promises that the checked-in Google test units will make CloudX lose, but the validation for this PR reports the opposite (platform=ADMOB). With zero ILRD revenue there is no meaningful price comparison, but the eventual winner can still depend on arbiter fallback/tie behavior; the README should not state a deterministic CloudX loss.
Which is why the AdMob ids checked in here make a poor price demo: Google's test
units report a revenue of 0.0, the price store drops any revenue of 0.0, and the
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.

lib/main.dart:81

  • When ATT is denied, this branch returns before MobileAds.instance.initialize() is started, but _adMobSdkStatus remains initializing forever. That makes the demo report an operation that never began; set the AdMob status to an explicit blocked/not-initialized value alongside the CloudX status.
  • Files reviewed: 46/74 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The comments explaining what reportRevenueData returns named ILRD telemetry and
the internal price store. That is SDK plumbing, and a public demo has no
business teaching it. The point a reader needs is smaller: the return value is
the call's result, not proof the price was kept, and a revenue of 0.0 is
discarded, which is what Google's test units pay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 11, 2026 12:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Four unresolved moderate review findings remain in the controller and demo UI.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

lib/main.dart:162

  • This resets both rows even though controller.load() intentionally starts only missing sides. After an ad is consumed, the other SDK still holds its fill, and after a none result both fills can already be held; in those cases no onAdLoaded callback follows for the retained side, so its status remains loading indefinitely and the diagnostic screen is incorrect. Preserve the retained status or expose/report per-side loading state and update only the sides that are actually requested.

lib/arbiter/arbiter_interstitial_controller.dart:237

  • An AdMob InterstitialAd is not valid indefinitely (loaded interstitials expire after about an hour), but this controller treats any non-null _adMobAd as a current bid. If the screen is left open or this side loses several rounds, an expired object can be selected here and show() fails instead of refreshing AdMob. Track the load timestamp/expiry and discard or reload stale ads before building bids.
    lib/main.dart:163
  • controller.show() claims _isShowing synchronously, but the widget can still have the old frame where Load both is enabled until an SDK callback triggers a rebuild. A tap in that window writes the loading/waiting rows before controller.load() returns because it is showing, leaving the diagnostic stuck or misleading. Guard the UI mutation with the controller's current state, not only the button's last build.
  • Files reviewed: 46/74 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread lib/main.dart Outdated
show() returns false for two different things: a winner that went stale, and a
show already in progress. _show() treated both as stale, so a second tap on the
still-live Show winner button set the rows to "loading" and called load(), which
declines to start anything while an ad is showing. The screen then sat behind
the ad describing work that was not happening, and one row stayed stuck there
after the ad closed.

isShowing tells the two apart, and the reload now happens only for a genuinely
stale winner. A successful show also rebuilds immediately instead of waiting for
the shown callback, so the button goes flat while the show is under way rather
than staying live in the gap, and it is disabled whenever isShowing is set.

This is the same defect as the Load both button in 37ae9a8, on the other button.
The earlier double-tap check missed it because the interstitial had already
covered the screen and swallowed the second tap; this one taps three times in
one shell round trip, which lands them before the ad renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 11, 2026 13:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved moderate issues remain in show recovery and demo state handling; initialization documentation also needs clarification.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

lib/main.dart:162

  • _load() marks both rows as loading, but controller.load() intentionally starts only missing sides. After an AdMob win is closed the CloudX fill is retained, and after a none/arbiter retry both fills can already be held, so no CloudX load callback follows and the row remains loading even after arbitration completes. Update only the sides actually being loaded (or expose a controller state/event for a re-arbitration) instead of resetting both rows here.

README.md:46

  • This copy-and-run instruction omits the required SDK initialization performed in main.dart: the controller does not initialize either SDK, and its own comments note that CloudX must already be initialized before loading. A consumer following this sentence can call load() before CloudX is ready, and on iOS also needs to complete ATT first; mention initializing Mobile Ads and CloudX (after the ATT decision where applicable) before calling load()/show().
A real integration copies the first three files and calls `load()` and `show()`.

lib/arbiter/arbiter_interstitial_controller.dart:177

  • The show slot is claimed before this call, but the actual CloudX show invocation is not awaited or protected by the error handling above. If the platform-channel call throws (or its returned Future fails), show() completes with an error while _isShowing remains true and the held ad is not cleared, so later load() calls are rejected permanently and the demo cannot recover. Handle this call like the readiness path: release the slot/fill and report onAdShowFailed on failure.
    lib/main.dart:81
  • When ATT is denied or restricted, this branch returns before MobileAds.instance.initialize() is called, but _adMobSdkStatus remains initializing forever. That makes the diagnostic screen report an operation that was never started; set the AdMob status to an explicit blocked/not-initialized value along with the CloudX status.
  • Files reviewed: 46/74 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The plugin version follows the iOS core, and the docs repo now declares
CloudXCore 3.9.0, so the demo moves with it: the pubspec constraint, the Podfile
pod, and both rows of the version table. Android stays on io.cloudx:sdk 4.7.0.

Resolved from the local pub host, which now serves the 3.9.0 archive: the lock
records source hosted with sha256 e35c268c0527c830cdad23599078f9b1f7b56c2b05eb99
b64f4664f89308a368, byte for byte the archive build-local-release.sh produced,
and the hermetic preflight confirms no path back into the monorepo.

Verified on an emulator and a simulator: pod install pulls CloudXCore 3.9.0,
flutter analyze is clean, both platforms build and run, and a round gives two
bids with an ADMOB winner on each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 11, 2026 13:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Two moderate findings remain unresolved, along with one dependency-version documentation nit.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

lib/main.dart:162

  • load() is intentionally incremental: it may reload only the missing side, or just rerun arbitration when both fills are already held (see arbiter_interstitial_controller.dart:87-112). This unconditional update makes both rows say loading even when neither row is loading or when one side remains held, and no callback will restore that retained side's status; after a none/arbiter-failure retry the screen can stay stuck showing work that is not happening. Preserve the existing row values or expose per-network loading state and only mark the sides that actually started loading.

lib/arbiter/arbiter_interstitial_controller.dart:177

  • The readiness call is protected, but this actual CloudX show invocation is not. If the listener claim or showInterstitial platform call throws synchronously, _isShowing remains true after _nextWinner was cleared, so later load()/show() calls are rejected forever and no failure event reaches the UI. Handle invocation failures here by releasing the show slot and reporting onAdShowFailed, just as the readiness failure path does.
    pubspec.yaml:25
  • The PR description still refers to cloudx_flutter 3.8.0 and says the dev block should be reverted to ^3.8.0, while this checked-in manifest and lockfile consistently resolve 3.9.0. Once the local host is removed, following the stated revert would not reproduce this dependency graph; update the description/revert instructions or align the manifest and lockfile.
  • Files reviewed: 46/74 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The README already promised this split and did not keep it. Its file table
called the controller "The integration" and main.dart "not part of the
integration", but three SDK calls lived in the screen's State: setMinLogLevel,
MobileAds.instance.initialize and CloudX.initialize. Those three are the
ordering a publisher most needs, and they were buried between setState calls.

lib/cloudx/ now holds every file that touches an SDK and lib/ui/ holds the
screen; main.dart is runApp and nothing else. The moves are pure renames. The
one piece of new code is sdk_startup.dart, which lifts the body of
_ArbiterScreenState._start() into a plain class with no setState and no mounted
checks, and states why ATT has to be answered before CloudX.initialize: CloudX
reads the tracking status at init and never asks for it, so initializing first
sends every request that session without an IDFA and with dnt = 1.

Following the public Unity and React Native demos, which both settled on this
shape: one directory is the copyable unit, the README names the copy list and
says what breaks if you take fewer, and each file repeats that instruction in
its own header so a file lifted out alone still says what it needs.

The boundary is checkable rather than asserted. No CloudX or AdMob call exists
outside lib/cloudx/, and nothing in lib/cloudx/ imports material.dart or builds
a widget. Two files keep a Flutter import for non-UI reasons: tracking_gate.dart
waits on AppLifecycleListener because iOS refuses to present the prompt while
the app is still becoming active, and the controller uses debugPrint. Both are
stated in their headers. Dropping the ATT package from the screen's imports was
the analyzer's own evidence that the split took.

Verified on an emulator and a simulator booted for this run, on both platforms:
ATT authorized, CloudX initialized, AdMob ready, two bids with an ADMOB winner,
the interstitial shown and closed, and 0.000000 USD reported (returned true).
A first Android attempt showed "initialization failed" from HTTP 503
{"error":"server draining"} on the CloudX init endpoint, which is server-side
and unrelated; it cleared on retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 11, 2026 16:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Three moderate issues remain in startup error handling, lifecycle handling, and refused-state UI reporting; two documentation nits also remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

lib/cloudx/sdk_startup.dart:89

  • This doc comment links to canLoadAds, but SdkStartupResult has no such member, so generated API docs point readers to a nonexistent contract. Describe the actual conditions instead of linking an undefined property.

README.md:55

  • These instructions name the wrong files to drop. From the five-file list above, an app that already initializes CloudX and handles ATT should remove sdk_startup.dart and tracking_gate.dart but retain demo_config.dart; the current wording tells readers to drop tracking_gate.dart and demo_config.dart while keeping startup, so the copied integration is not usable as documented.
**Take fewer and it will not build.** The controller reports through
`arbiter_events.dart`, and `sdk_startup.dart` calls `tracking_gate.dart`. If
your app already initializes CloudX and answers the ATT prompt, drop those last
two and keep the first three.

lib/cloudx/sdk_startup.dart:34

  • run() documents that it never throws, but TrackingGate.request() performs awaited platform-channel calls here without any error handling. If the ATT plugin reports a native/channel failure, _start() rejects and the screen never receives a startup result or failure status; handle this failure and return/report a failed startup outcome just as CloudX.initialize is handled below.
    lib/cloudx/tracking_gate.dart:55
  • There is a lifecycle race between this state check and registering the listener: if the app resumes in that window, no later onResume event is guaranteed, so _waitUntilActive() can remain pending forever and the demo stays at "requesting tracking permission". Recheck binding.lifecycleState after installing the listener and complete the completer when it is already resumed.
  • Files reviewed: 48/76 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread lib/ui/arbiter_screen.dart
SdkStartup.run returns before it starts Google Mobile Ads when the ATT prompt is
refused, but the screen's refusal branch only updated the CloudX row. The Google
Mobile Ads row kept its initial "initializing" and sat there for the life of the
app, describing an initialization that was never started. It now says so.

Verified on the simulator rather than reasoned: simctl privacy reset all, then
"Ask App Not to Track" on the prompt. Tracking reads "denied", CloudX reads
"tracking not authorized - ads cannot load", Google Mobile Ads reads "not
started - tracking not authorized", and both buttons stay disabled. That run
also exercises the ATT gate's refusal path, which had not been covered in this
session.

Fifth finding in this PR of one shape: a status row asserting something the
screen does not know.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 11, 2026 16:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The unresolved moderate startup error-handling finding can leave initialization incomplete.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (6)

Previously missed (1) — in code that hasn't changed since the last review.

lib/cloudx/sdk_startup.dart:90

  • This doc comment links to canLoadAds, but SdkStartupResult has no such member and the symbol does not exist elsewhere in the project. The generated API documentation is therefore broken and the comment suggests a gating API that this result does not expose; describe cloudXInitialized directly instead.

README.md:165

  • This table says both platform rows show loaded: <network> $<price>, but the AdMob loaded callback passes only _adMobNetworkName (arbiter_interstitial_controller.dart:380); no AdMob price is displayed. Please document the two formats accurately so readers do not expect a value that the screen cannot show.
| `CloudX` / `AdMob` | That side's last event: `loading`, `loaded: <network> $<price>`, `load failed: ...`, `showing`, `closed`. |

README.md:34

  • The README says to expect CloudX to lose with these test units, but the validation described for this PR reports an ADMOB winner with the checked-in units. That makes the demo guidance misleading; describe the zero revenue as meaning there is no learned AdMob price, without predicting which bid wins.
Which is why the AdMob ids checked in here make a poor price demo: Google's test
units report a revenue of 0.0, the price store drops any revenue of 0.0, and the
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.

README.md:55

  • This copy instruction says an app can drop sdk_startup.dart after only initializing CloudX and answering ATT, but that file also initializes Google Mobile Ads. Following the documented recipe leaves AdMob uninitialized before InterstitialAd.load() is used; require both SDKs to be initialized, or tell the reader to retain/replace the AdMob initialization.
**Take fewer and it will not build.** The controller reports through
`arbiter_events.dart`, and `sdk_startup.dart` calls `tracking_gate.dart`. If
your app already initializes CloudX and answers the ATT prompt, drop those last
two and keep the first three.

README.md:50

  • The PR description's “What is in the diff” section still points at lib/arbiter/..., lib/config/..., lib/tracking_gate.dart, and calls lib/main.dart the demo screen, but the checked-in files are under lib/cloudx/ and lib/ui/ (main.dart is only the entry point). Please update the description so its file map matches the code reviewers and users will actually find.
| [`lib/cloudx/arbiter_interstitial_controller.dart`](lib/cloudx/arbiter_interstitial_controller.dart) | The integration. The whole load/arbitrate/show cycle and both SDKs' calls, in one file. |
| [`lib/cloudx/arbiter_events.dart`](lib/cloudx/arbiter_events.dart) | The callbacks the controller reports through. |
| [`lib/cloudx/sdk_startup.dart`](lib/cloudx/sdk_startup.dart) | Brings both SDKs up, in the order they have to come up in. |
| [`lib/cloudx/tracking_gate.dart`](lib/cloudx/tracking_gate.dart) | The iOS App Tracking Transparency gate. |
| [`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. |

lib/cloudx/sdk_startup.dart:34

  • SdkStartup.run() promises never to throw, but TrackingGate.request() is the only startup await still outside a handler. Either ATT platform-channel call can reject (for example if the plugin/native API is unavailable); that rejection escapes _start() and leaves the screen's initial statuses forever. Catch it and return a refused/failed result before starting either SDK.
  • Files reviewed: 48/76 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Two things the SDK gained in 3.9.0 reach the screen here.

CloudX.initialize now returns a result instead of a nullable
configuration, so a failed start carries the SDK's own error code, its
name and a message rather than being a bare null. The load and display
failures carry the same name through CloudXError.codeName. Both are
formatted in one new file, cloudx_failure_text.dart, so the two paths
cannot drift: a round that did not fill now reads
"No ad available. (NO_FILL[302])" and a rejected app key reads
"... (NETWORK_CLIENT_ERROR[103])" instead of a bare number. The message
is trimmed because the Android initialize failure arrives with a
trailing newline, which otherwise pushed the code onto its own line.

io.cloudx:adapter-bigo:6.0.1.0 joins the Android adapter set, which the
README describes as the full CloudX set and which was missing it. The
adapter needs cleartext traffic to 127.0.0.1, because the BIGO Ads SDK
serves some creative assets from a loopback server on the device, so
res/xml/network_security_config.xml and the manifest reference go with
it. BIGO is Android only; there is no iOS adapter, which is why the
Gradle file now lists one network more than the Podfile.

Verified on a Pixel 10 Pro, an emulator and an iOS simulator: two bids
per round with ADMOB winning, and forced failures showing the named
codes on both platforms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 12, 2026 20:26
@antonurankar-moloco antonurankar-moloco changed the title Add the Trusted Arbiter interstitial demo Bootstrap the public demo: Trusted Arbiter for interstitials Sep 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

CloudX initialization and ATT failures can still escape the startup error handling and leave the demo stuck.

Get a fresh assessment by requesting another Copilot review.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (2)

README.md:57

  • This copy guidance only mentions an existing CloudX initialization and ATT prompt, but sdk_startup.dart also initializes Google Mobile Ads. A consumer following these instructions can omit MobileAds.instance.initialize() when removing the startup helper; include Google Mobile Ads in the prerequisite.
If your app already initializes CloudX and answers the ATT prompt, drop
`sdk_startup.dart` and `tracking_gate.dart` and keep the rest.

lib/cloudx/sdk_startup.dart:37

  • The same never throws contract is still violated by TrackingGate.request(): it awaits platform-channel status/prompt calls without handling rejection. If ATT fails before returning a status, _start() aborts and the screen remains at requesting tracking permission; convert this to a refused/failed startup result (and log it) instead of letting the future escape.
  • Files reviewed: 50/78 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread lib/cloudx/sdk_startup.dart
Two findings from a line-by-line pass over this PR.

The instruction for undoing the dev setting was wrong in two ways, and both
were checked rather than reasoned. `git revert 833a3e6` does not apply: it
conflicts on pubspec.yaml and modify/deletes pubspec.lock, because two later
commits rewrote the lock. And its parent is not the state to return to - that
commit declares `cloudx_flutter: ^3.8.0` and the pre-review Dart and Flutter
floors, so taking it would undo the pin bump and the floor correction that
came out of the first Copilot round. The lock is the part that actually
matters: it still records `url: "http://127.0.0.1:8080"` with a sha256 only
the local host serves, so it has to be regenerated whatever happens to the
manifest. The comment now says to edit the block and re-resolve.

The README called banners "a different (inline) flow". Inline is what the SDK
calls CloudXAdView, which 3.9.0 leaves unexported, so that sent a reader
looking for an API that is not public. Banners are described by what they are
instead: a view createBanner places at a fixed screen position.

flutter analyze clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 12, 2026 20:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Two moderate startup error-handling issues remain unresolved, and the failure-code documentation nit needs clarification.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (3)

lib/cloudx/cloudx_failure_text.dart:10

  • This copy-facing example conflicts with the demonstrated bad-app-key behavior in the PR description, which reports NETWORK_CLIENT_ERROR[103] for the Android initialize path. Either use the observed code here or explicitly explain the platform/error-path distinction; otherwise readers can be led to diagnose a different failure code than the demo displays.
    lib/cloudx/sdk_startup.dart:37
  • run() promises never to throw, but TrackingGate.request() is awaited outside any error handling. A plugin/platform-channel failure while reading or requesting ATT will reject run() and _start() does not catch it, leaving the startup rows at their initial values and producing an unhandled async error. Catch this path and return/report an explicit startup failure (or handle it in the caller).
    lib/cloudx/sdk_startup.dart:66
  • SdkStartup.run promises never to throw, but this CloudX.initialize await is still outside a try/catch. A native/platform-channel exception rejects _start() before it can update either SDK status, leaving the screen stuck at its initial rows and violating the startup result contract. Catch the exception and return a failed SdkStartupResult (or otherwise surface it as an initialization failure).
  • Files reviewed: 50/78 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Taking the public Unity demo's lead on both.

That README carries six screenshots out of docs/images/, each one next to the
section that explains it. This one had none, so a publisher deciding whether to
read further had nothing to look at. Three are added the same way: the ATT
prompt beside the step about it, and the settled screen plus the interstitial it
shows beside the table that explains the rows. The ATT one earns its place by
showing the ordering rule rather than the prompt - CloudX SDK still reads "not
initialized" behind it, which is the whole reason tracking_gate.dart exists.

The signing note was filed under Versions, three lines after a paragraph about
Dart and Flutter SDK floors. The Unity demo groups app key, bundle id and
signing team together under "Using your own CloudX app", because those are the
three things a publisher changes to make the demo theirs. Signing is now step 5
of Required setup, which is where someone hits it, at their first device build.

Screenshots are from this branch's head on an emulator and a simulator I booted;
244 KB for all three.

flutter analyze clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 12, 2026 21:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The ATT lifecycle race and retry-time winner replacement remain unresolved, and the platform-specific failure-code example needs clarification.

Get a fresh assessment by requesting another Copilot review.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (1)

lib/cloudx/arbiter_interstitial_controller.dart:88

  • load() does not guard an already prepared winner. If one side failed and the other produced _nextWinner, a caller that retries load() before showing starts the missing side; when it fills, _handleCloudXLoaded/the AdMob callback calls _maybePrepareWinner() and can replace the winner that was ready to show. The demo UI currently prevents this, but the copied controller exposes load() publicly, so keep the prepared-winner state immutable until show() consumes it.
  • Files reviewed: 50/81 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread lib/cloudx/tracking_gate.dart
Comment thread lib/cloudx/cloudx_failure_text.dart Outdated
Copilot, round 3, and it caught the repository disagreeing with itself.

cloudx_failure_text.dart offered "a rejected app key says INVALID_APP_KEY[203]"
as its worked example. The PR body's own validation section records the opposite
from a deliberately broken key: NETWORK_CLIENT_ERROR[103], because what failed
was the HTTP response rather than a key the SDK recognised and rejected. 203 is
a real constant, so the name is not invented, but nothing in this demo produces
it and a reader would wait for a code that never comes.

The example now uses the two codes the forced-failure runs produced on both
platforms, NO_FILL[302] and INVALID_AD_UNIT[300], and says outright that
choosing the code is the SDK's business: read the name it sent rather than
predicting which one a failure will carry. That is the file's whole argument
anyway.

Also records, in tracking_gate.dart, why reading lifecycleState and then
attaching the listener is not the race it looks like. Copilot read it as one;
so did I, in an earlier round, before working it out. There is no await between
the two statements and Dart only turns the event loop at a suspension point, so
no lifecycle message can land in between - and AppLifecycleListener calls
onResume unconditionally on the transition to resumed, including from a null
previous state, so a first-ever resumed still fires it. Two readers have now
had to derive that; a file meant to be copied should say it.

No behaviour change. flutter analyze clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQvJAmT4AgvqCdgQECTjcV
Copilot AI review requested due to automatic review settings September 12, 2026 21:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad cross-platform SDK and native-project changes warrant final human review, and a documentation nit remains.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (1)

lib/ui/arbiter_screen.dart:57

  • This comment says the controller is built once both SDKs are up, but SdkStartup.run() deliberately returns before MobileAds.initialize() completes and only awaits CloudX initialization. The controller can therefore be created while AdMob is still initializing; describe that it is created after CloudX is ready while AdMob initialization continues in parallel, so copied integrations do not infer a stronger ordering than the code provides.
  • Files reviewed: 50/81 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

cloudx_flutter 3.9.0 was published today, so the demo no longer needs the
local pub host the dev setting pointed it at. The manifest goes back to a
plain version constraint and pubspec.lock records pub.dev with the sha256
of the published archive.

Not done by reverting the commit that introduced the dev setting: two later
commits rewrote pubspec.lock, so the revert conflicts, and that commit's
parent also carries the older ^3.8.0 pin and the pre-review Dart and Flutter
floors. The pubspec comment prescribed this path.

Verified against a clean room: a fresh clone resolved with an empty
PUB_CACHE created only a pub.dev entry and produced a byte-identical lock.
The published lib/, android/ and ios/ are identical to the flutter/sdk/3.9.0
tag, and ios/Podfile.lock is unchanged. One full arbiter round ran on an
Android emulator and an iOS simulator: two bids, ADMOB won, ad shown,
reportRevenueData returned true.
The BIGO network security config declared 127.0.0.1 with no
includeSubdomains attribute, which Android lint reports as an error under
lintVital. That task runs on every release assemble, so
`flutter build apk --release` failed outright:

    android/app/src/main/res/xml/network_security_config.xml:12: Error:
    Missing includeSubdomains attribute [NetworkSecurityConfig]

The value changes no behaviour: 127.0.0.1 is a literal address with no
subdomains to include, and false is the attribute's default. What matters
to lint is that the choice is explicit. The form here matches the adapter's
own integration note in the monorepo, which has always carried it.

Verified by building the release APK: app-release.apk at 68.5MB.
Copilot AI review requested due to automatic review settings September 13, 2026 20:32
@antonurankar-moloco
antonurankar-moloco merged commit 81b5dc3 into main Sep 13, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolve the unavailable cloudx_flutter dependency source and preserve a prepared winner when load() is called again.

Get a fresh assessment by requesting another Copilot review.

Review details

Files not reviewed (2)

  • ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Generated file
  • ios/Runner.xcworkspace/contents.xcworkspacedata: Generated file

Suppressed comments (3)

README.md:58

  • SdkStartup.run also starts Google Mobile Ads via MobileAds.instance.initialize() (sdk_startup.dart:47), but this copy instruction only says an existing app must initialize CloudX and answer ATT before dropping sdk_startup.dart. Following it can remove the only AdMob initialization while the controller still calls InterstitialAd.load; mention Google Mobile Ads here or require callers to initialize it separately.
If your app already initializes CloudX and answers the ATT prompt, drop
`sdk_startup.dart` and `tracking_gate.dart` and keep the rest.

README.md:127

  • This sentence reverses the ordering stated immediately before it: the example correctly says the ATT prompt must be answered before CloudX initialization, but then says “initialize first” without making clear that this is the mistake being warned against. A reader can follow that as an instruction and initialize while ATT is still not determined, causing the exact IDFA/dnt behavior this setup avoids; phrase it as a conditional warning.
   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`.

lib/cloudx/arbiter_interstitial_controller.dart:88

  • load() is still callable while a winner is prepared. In the one-sided-fill case, arbitration can store a CloudX winner while the AdMob side is null; calling load() then starts AdMob, and its callback invokes _maybePrepareWinner(), which can overwrite or clear the existing winner. The UI disables this path, but the public controller contract should preserve a prepared winner for callers that invoke load() directly; return early when _nextWinner != null (or otherwise isolate a new round).
  • Files reviewed: 50/81 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread pubspec.yaml
flutter:
sdk: flutter

cloudx_flutter: ^3.9.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants