feat: Custom Screens (BYOS) support for the Flutter SDK - #134
Conversation
Greptile SummaryThis PR migrates the Flutter bridge to the native v6 APIs and adds Custom Screen support. The main changes are:
|
| Filename | Overview |
|---|---|
| purchasely/lib/src/custom_screens.dart | Adds the Custom Screen entrypoint, presentation wrapper, widget host, and navigation channel. |
| purchasely/lib/src/bridge.dart | Adds v6 presentation lifecycle, interceptor, event-routing, and Custom Screen bridge behavior. |
| purchasely/android/src/main/kotlin/io/purchasely/purchasely_flutter/PurchaselyCustomScreenFragment.kt | Hosts Android Custom Screens in dedicated Flutter engines with keyed bridge routing and cleanup. |
| purchasely/android/src/main/kotlin/io/purchasely/purchasely_flutter/PurchaselyFlutterPlugin.kt | Implements the Android v6 bridge and Custom Screen provider, but broadens handle-scoped close into a global close. |
| purchasely/ios/Classes/PurchaselyCustomScreenViewController.swift | Hosts iOS Custom Screens in secondary Flutter engines and manages their bridge lifecycle. |
| purchasely/ios/Classes/SwiftPurchaselyFlutterPlugin.swift | Implements the iOS v6 presentation, interceptor, dismiss, and Custom Screen contracts. |
| docs/plans/custom-screens-byos.md | Documents the Custom Screen architecture, API, wire protocol, milestones, trade-offs, and open questions. |
Sequence Diagram
sequenceDiagram
participant App as Host Flutter App
participant Bridge as Flutter Bridge
participant Native as Native Purchasely SDK
participant Screen as Custom Screen Engine
App->>Bridge: setCustomScreenProvider(entrypoint)
Bridge->>Native: Register provider
Native->>Bridge: Request CLIENT screen
Bridge->>Screen: Start entrypoint(customScreenId)
Screen->>Bridge: Load presentation data
Bridge-->>Screen: Metadata and connections
Screen->>Bridge: execute / back / close
Bridge->>Native: Update native flow
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
purchasely/android/src/main/kotlin/io/purchasely/purchasely_flutter/PurchaselyFlutterPlugin.kt:631-635
**Handle Close Becomes Global Close**
Dart passes the loaded presentation's `requestId`, but Android ignores it and calls `closeAllScreens()`. When an app-owned presentation and an SDK-opened campaign overlap, closing either handle dismisses both screens and emits outcomes for the unrelated presentation; iOS preserves the handle-scoped behavior by resolving `loadedPresentations[requestId]`.
Reviews (1): Last reviewed commit: "feat(example): point the Custom Screen d..." | Re-trigger Greptile
| private fun closePresentation(args: Map<String, Any?>?, result: Result) { | ||
| // The native SDK does not expose per-presentation programmatic close; | ||
| // close all screens regardless of whether a requestId was provided. | ||
| Purchasely.closeAllScreens() | ||
| result.safeSuccess(true) |
There was a problem hiding this comment.
Handle Close Becomes Global Close
Dart passes the loaded presentation's requestId, but Android ignores it and calls closeAllScreens(). When an app-owned presentation and an SDK-opened campaign overlap, closing either handle dismisses both screens and emits outcomes for the unrelated presentation; iOS preserves the handle-scoped behavior by resolving loadedPresentations[requestId].
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/android/src/main/kotlin/io/purchasely/purchasely_flutter/PurchaselyFlutterPlugin.kt
Line: 631-635
Comment:
**Handle Close Becomes Global Close**
Dart passes the loaded presentation's `requestId`, but Android ignores it and calls `closeAllScreens()`. When an app-owned presentation and an SDK-opened campaign overlap, closing either handle dismisses both screens and emits outcomes for the unrelated presentation; iOS preserves the handle-scoped behavior by resolving `loadedPresentations[requestId]`.
How can I resolve this? If you propose a fix, please make it concise.Render Flutter UI for a CLIENT step inside a native Purchasely flow via a secondary FlutterEngine (dedicated isolate) and a purchasely-custom-screen MethodChannel bridging to a customScreenId-keyed native registry. Adds PLYConnection, connections/customScreenId/metadata on PLYPresentation, execute() navigation, Purchasely.setCustomScreenProvider, PurchaselyCustomScreens.run, example demo, docs, and tests. Includes the code-review fixes (Android registry-leak guard, iOS engine teardown timing, graceful Dart channel error handling, deterministic iOS connection order, customScreenExecuteConnection channel method). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a0c9fd1 to
06583aa
Compare
What
Adds the Custom Screens (BYOS) feature to the Flutter SDK — Flutter-authored UI for
CLIENTsteps inside native Purchasely flows. When a flow reaches anis_clientscreen, the SDK asks the app for UI via a provider and embeds it inside the flow container (inheriting the step transition); the app drives navigation by executing connections.Tracked by MOB-203 (parent MOB-200 "Pause/Resume flow → BYOS Bridges"). Plan:
docs/plans/custom-screens-byos.md.Custom Screens implementation
Purchasely.setCustomScreenProvider({entrypoint, libraryUri})/removeCustomScreenProvider(),PurchaselyCustomScreens.run(args, builder),PLYConnection, andexecute/back/closeonPLYCustomScreenPresentation.FlutterEngine(viaFlutterEngineGroup) runs a dedicated@pragma('vm:entry-point')entrypoint where the app registers a widget builder; the native provider (PLYCustomScreenProvideron Android,PLYCustomScreenViewControllerDelegateon iOS) returns a hostFlutterFragment/FlutterViewController. The isolate-isolation trade-off (no shared app state) is documented.purchasely-custom-screenMethodChannel bridges the spawned isolate to acustomScreenId-keyed native registry (getCustomScreenPresentation/customScreenExecuteConnection/customScreenBack/customScreenClose), modeled on the existing action-interceptor round trip.sdk_public_doc.md, and an example demo against placementbyos.Code review (Custom Screens subset)
Ran a multi-persona review of the custom-screens change. Fixes applied, committed as
fix(review): …, and verified —flutter analyzeclean, 249 Dart tests pass, Android debug APK + iOS simulator builds succeed:args["connectionId"]used the index operator on a nullable map (would fail the Android build)willMove(toParent:nil)todeinitcustomScreenExecuteConnectionchannel method; deterministic iOS connection ordering;sdk_public_doc.mdTOC fixOpen follow-ups (not yet addressed)
metadatais now serialized on every presentation event with no codec coercion. Verify SDK metadata values areStandardMessageCodec-safe (else coerce/gate to client steps) + add a native round-trip test — otherwise a non-primitive value can break normal paywalls, not just custom screens.execute()/customScreenIdbelong on the basePLYPresentation(currently a foot-gun on non-client presentations + a source-breaking abstract method); confirm/document the iOSscreenIdsource change (p.id→p.screenId).🤖 Generated with Claude Code