refactor: migrate the Flutter plugin to the Purchasely 6.0 native SDK#120
refactor: migrate the Flutter plugin to the Purchasely 6.0 native SDK#120kherembourg wants to merge 21 commits into
Conversation
Introduces the Dart-side v6 façade per BRIDGE-CONTRACT.md: - Presentation, PresentationBuilder, PresentationRequest - PresentationOutcome (5-field enriched result) - ActionInterceptor with typed actions - Transition (animation/transition options) - RequestId (correlation id for bridge calls) - PurchaselyBuilder (top-level v6 entrypoint) These types are platform-agnostic and form the contract the iOS/Android Flutter bridges will implement via MethodChannel/EventChannel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds re-exports for the v6 cross-platform façade (Presentation, PresentationBuilder, PresentationRequest, PresentationOutcome, Transition, action interceptor types, PurchaselyBuilder) so callers get the full v6 API by importing the package entry point. The v6 builder enums clash by name with two legacy v5 enums (`PLYRunningMode` had 4 values in v5, `PLYLogLevel` had the same 4 in v5) so they are renamed `V6RunningMode` / `V6LogLevel` to allow both APIs to co-exist during the migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new PurchaselyV6Bridge.kt that dispatches `v6/*` MethodChannel calls against the v6 Android SDK builder DSL (PLYPresentationBase), emits lifecycle callbacks (`onLoaded`, `onPresented`, `onCloseRequested`, `onDismissed`) and interceptor invocations on a new `purchasely/v6-events` EventChannel, and round-trips interceptor results via `v6/interceptorResolve`. Bumps the native dependency `io.purchasely:core` to `6.0.0` (v6 SDK Builder DSL + `PLYPresentationBase`/`PLYPresentationAction` sealed class). Adjusts the legacy v5 start callback path to match the v6 single-arg `(PLYError?) -> Unit` callback shape and collapses the v5 PaywallObserver/TransactionOnly running modes onto v6 `PLYRunningMode.Observer`. The existing v5 surface (`Purchasely.start`, `fetchPresentation`, etc.) is left intact; the v6 bridge runs alongside it so apps can migrate incrementally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…karounds
Adds PurchaselyV6Bridge.swift dispatching `v6/*` MethodChannel calls
against the v6 iOS SDK (PurchaselyBuilder, PLYPresentationBuilder /
PLYPresentationRequest, interceptAction). Lifecycle and interceptor
events are emitted on the new `purchasely/v6-events` EventChannel and
results round-trip through `v6/interceptorResolve`.
Bridge workarounds per BRIDGE-CONTRACT.md:
* P0.1 — iOS exposes `onClose`; emitted on the wire as
`onCloseRequested` so the Dart façade matches Android.
* P0.2 — iOS `PLYPresentationOutcome` has only `purchaseResult` + `plan`;
the 5-field enriched outcome (`presentation`, `closeReason`, `error`)
is synthesised here. `closeReason` is `nil` until the native fix lands.
* P0.3 — `display(...)` completion fires at trigger time, not dismiss
time; the Dart-side `.display()` Future resolves from the
`onDismissed` event, not from this completion handler.
* P0.4 — when the display/preload completion delivers an error, the
bridge synthesises `onPresented(nil, error)` and an error outcome so
Dart callbacks fire uniformly across platforms.
* P1.1 — Dart `screen(screenId)` maps to iOS
`PLYPresentationBuilder.from(presentationId:)`; iOS `presentation.id`
is emitted as `screenId` on the wire.
Bumps the iOS pod dependency to `Purchasely 6.0.0`. The existing v5
SwiftPurchaselyFlutterPlugin is left in place and dispatches v6 calls to
the new bridge before falling through to legacy handlers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a v6 demo screen (`example/lib/v6_demo_screen.dart`) showing the canonical v6 flow: * SDK init via `PurchaselyBuilder.apiKey(...).start()` * Display via `PresentationBuilder.placement(...).build().display(...)` * Lifecycle callbacks: onLoaded, onPresented, onCloseRequested, onDismissed * Enriched 5-field `PresentationOutcome` rendered as a card A placeholder for typed `interceptAction(navigate, ...)` is wired to a button; the actual cross-bridge interceptor dispatcher lives on the Dart façade side and ships separately. The legacy v5 example screens are kept intact — a new "Open v6 demo" button on the home screen routes to the new demo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README: documents the new v6 builder-based API as the primary usage path, with a v5 → v6 migration table and a legacy v5 section kept for reference. - CHANGELOG: adds the 6.0.0-beta.0 entry covering the new cross-platform façade, bridge contract workarounds, native SDK bumps, and breaking changes (Observer is now the default running mode). - pubspec.yaml: bumps `purchasely_flutter` to `6.0.0-beta.0`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces `lib/src/bridge.dart` (PurchaselyV6Bridge) which routes the v6 Dart façade calls to the `purchasely` MethodChannel (`v6/*` verbs) and dispatches lifecycle events from the `purchasely/v6-events` EventChannel back to the request/presentation callbacks per the BRIDGE-CONTRACT v3-claude. Hooks `PresentationActions.instance` and `PresentationRequestActions.instance` once any v6 entry point is invoked (lazy install in `PurchaselyBuilder.start()` and `PresentationBuilder.build()`). - Routes preload/display/close/back to native, decodes Presentation + PresentationOutcome maps, surfaces PlatformException as PresentationError. - `display()` awaits the native `onDismissed` event (matches P0.3 — Promise resolves at DISMISS, not trigger). - Interceptor pipeline: registers handlers on the Dart side, awaits `interceptorTriggered` events, resolves via `v6/interceptorResolve` (mapped to PLYInterceptResult). - Exposes `PurchaselyV6Bridge.ensureInstalled` / `.debugReset` to allow channel injection in tests. Adds `test/bridge_test.dart` (4 tests) covering preload args, display-awaits-dismiss, onLoaded callback firing and Transition serialization. Full suite: 267 tests pass, `flutter analyze` clean. Known native gaps (not in scope of this commit): - Android `v6/close` ignores `requestId` and globally calls `closeAllScreens()` — per-presentation programmatic close is not yet exposed by the SDK (already documented in PurchaselyV6Bridge.kt). - iOS bridge will need to surface `onLoaded` events explicitly for the Dart-side `onLoaded` callback to fire post-preload (currently the preload completion handler is the only signal — Dart treats the MethodChannel response as the loaded state, so this works, but a parallel `onLoaded` event would let the request-level callback fire with the iOS-synthesized PresentationError on load failure). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fecycle Extends test/bridge_test.dart from 4 to 9 tests: - Outcome 5 fields with closeReason (P0.2) - Outcome with error and null closeReason (P0.2 mutual exclusion) - onCloseRequested fires builder callback - Interceptor lifecycle: register → trigger → resolve via invocationId - removeInterceptor unregisters the kind Parity with React Native v6 integration tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| purchasely/ios/Classes/PurchaselyV6Bridge.swift | iOS bridge synthesising the 5-field contract. Display-error path emits events via DispatchQueue.main.async but calls result(FlutterError) synchronously, so P0.4 onPresented(nil,error) callbacks are silently swallowed by the Dart PlatformException handler. Also hardcodes NSNull() for contentId. |
| purchasely/android/src/main/kotlin/io/purchasely/purchasely_flutter/PurchaselyV6Bridge.kt | Android bridge implementing the full v6 contract. displayCallbacks entry leaks on synchronous display errors; loadedPresentations/preparedRequests grow unboundedly across the session. Logic is otherwise correct. |
| purchasely/lib/src/bridge.dart | Core Dart dispatcher wiring MethodChannel/EventChannel to the v6 façade. Entry lifecycle (register, re-display, dismiss) is solid; the PlatformException guard prevents double-completion. No new issues found. |
| purchasely/lib/src/action_interceptor.dart | Typed action payload hierarchy and wire serialisation. Consistent with both native bridges; null-guarded parsing for required fields. |
| purchasely/test/bridge_test.dart | 5 new integration tests covering preload, display, callbacks, interceptor lifecycle, and re-display regression. Good coverage of the happy path and the previously-flagged re-display hang. |
| purchasely/lib/src/presentation_outcome.dart | 5-field outcome model with correct closeReason/error mutual-exclusion semantics; handles both camelCase and snake_case variants for backSystem. |
Sequence Diagram
sequenceDiagram
participant App as Flutter App
participant Dart as PurchaselyV6Bridge (Dart)
participant MC as MethodChannel purchasely
participant EC as EventChannel purchasely/v6-events
participant Native as Native Bridge Android/iOS
App->>Dart: display()
Dart->>MC: "v6/display {requestId, transition}"
MC->>Native: handle v6/display
Native-->>MC: result(true)
MC-->>Dart: invokeMethod resolves OK
Note over Dart: completer stored awaiting dismiss
Native-->>EC: "onPresented {requestId, presentation}"
EC-->>Dart: _handleOnPresented fires callback
Native-->>EC: "onDismissed {requestId, outcome}"
EC-->>Dart: completer.complete(outcome)
Dart-->>App: Future resolves with PresentationOutcome
Note over Native,Dart: iOS error path P0.4 issue
Native-->>EC: onPresented nil error async main queue
Native-->>EC: onDismissed error outcome async main queue
Native-->>MC: result(FlutterError) synchronous
MC-->>Dart: PlatformException entry removed completer complete
EC-->>Dart: onPresented arrives entry null SKIPPED
EC-->>Dart: onDismissed arrives entry null SKIPPED
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
purchasely/ios/Classes/PurchaselyV6Bridge.swift:272-299
**P0.4 onPresented synthesis never fires on iOS display errors**
`events.emit(...)` wraps its payload in `DispatchQueue.main.async`, so the `onPresented` and `onDismissed` events are queued for a future run-loop iteration. But `result(FlutterError(...))` is called synchronously in the same closure and is processed first by Dart. The `_displayPresentation`/`_displayRequest` PlatformException handler removes the entry from `_entries` and completes the dismiss completer before either async event is delivered. When the events eventually arrive, `_handleOnPresented` and `_handleOnDismissed` find `entry == null` and return early — the builder's `onPresented(nil, error)` callback never fires.
Changing the error path to call `result(true)` instead of `result(FlutterError(...))` would let both events be processed while the entry is still live, matching the behavior documented in BRIDGE-CONTRACT P0.4 and aligning with Android's event-only outcome delivery.
### Issue 2 of 4
purchasely/ios/Classes/PurchaselyV6Bridge.swift:386
**iOS `contentId` always serialised as `null`**
All other optional fields (`placementId`, `audienceId`, `abTestId`, etc.) pass through their `PLYPresentation` values with `as Any`, but `contentId` is unconditionally `NSNull()`. If `PLYPresentation` exposes a `contentId` property in the v6 SDK, this silently drops the value; `Presentation.contentId` will always be `nil` on iOS even when the backend set one.
```suggestion
"contentId": p.contentId as Any, // TODO: verify contentId is exposed in v6 SDK
```
### Issue 3 of 4
purchasely/android/src/main/kotlin/io/purchasely/purchasely_flutter/PurchaselyV6Bridge.kt:264-275
**`displayCallbacks` entry leaks on synchronous display error**
`displayCallbacks[requestId]` is set before the `try` block. If `prepared.display(...)` throws synchronously, the catch block calls `result.error(...)` but never removes the `displayCallbacks[requestId]` entry. The key lives in the map indefinitely, holding a no-op lambda. Adding `displayCallbacks.remove(requestId)` inside the catch block closes the leak.
### Issue 4 of 4
purchasely/android/src/main/kotlin/io/purchasely/purchasely_flutter/PurchaselyV6Bridge.kt:209-218
**`loadedPresentations` and `preparedRequests` grow unboundedly**
`loadedPresentations[requestId]` is populated in `v6Preload` and `preparedRequests[requestId]` in `buildPrepared`, but neither map is pruned when a presentation is dismissed (`buildPrepared.onDismissed` only removes from `displayCallbacks`). Because `nextRequestId()` generates a fresh UUID for every `PresentationBuilder.build()` call, each displayed presentation leaves a permanent `PLYPresentation` reference for the lifetime of the bridge. The same applies to `presentations` on the iOS side.
Reviews (3): Last reviewed commit: "chore(dart): apply dart format to v6 dem..." | Re-trigger Greptile
- bridge.dart: re-display after dismiss no longer hangs — _displayPresentation now re-registers the request entry (keyed by requestId) from the Presentation handle so the dismiss completer is always stored. _RequestEntry.request is now nullable; handlers guard accordingly. Adds a regression test (P1). - PurchaselyV6Bridge.kt: drop dead if/else in v6Close (both branches called closeAllScreens) and collapse identical errorToMap branches; remove now-unused PLYError import. - PurchaselyV6Bridge.swift: outcomeToMap now threads the real requestId into the nested presentation map instead of an empty string. https://claude.ai/code/session_01TMtx4cHizaTD3TR77MD1Vk
|
@greptileai review Generated by Claude Code |
parseTransition fell through to else -> null for the inlinePaywall wire value, so a Dart caller passing Transition(type: TransitionType.inlinePaywall) got the SDK default transition on Android while iOS correctly mapped it to .inlinePaywall. Map "inlinePaywall" to PLYTransitionType.INLINE_PAYWALL to restore cross-platform parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@greptileai review |
| } else if let error = error { | ||
| // P0.4 — synthesise onPresented(nil, error) so the Dart-side | ||
| // builder onPresented handler fires uniformly across platforms. | ||
| self.events.emit([ | ||
| "event": "onPresented", | ||
| "requestId": requestId, | ||
| "presentation": nil as Any?, | ||
| "error": Self.errorToMap(error), | ||
| ]) | ||
| // Also synthesise onDismissed with the 5-field error outcome. | ||
| let outcome = self.outcomeToMap( | ||
| PLYPresentationOutcome(purchaseResult: .none, plan: nil), | ||
| presentation: nil, | ||
| error: error, | ||
| requestId: requestId | ||
| ) | ||
| self.events.emit([ | ||
| "event": "onDismissed", | ||
| "requestId": requestId, | ||
| "outcome": outcome, | ||
| ]) | ||
| result(FlutterError(code: "V6_DISPLAY", | ||
| message: error.localizedDescription, | ||
| details: Self.errorToMap(error))) | ||
| } else { | ||
| result(true) | ||
| } | ||
| } |
There was a problem hiding this comment.
P0.4 onPresented synthesis never fires on iOS display errors
events.emit(...) wraps its payload in DispatchQueue.main.async, so the onPresented and onDismissed events are queued for a future run-loop iteration. But result(FlutterError(...)) is called synchronously in the same closure and is processed first by Dart. The _displayPresentation/_displayRequest PlatformException handler removes the entry from _entries and completes the dismiss completer before either async event is delivered. When the events eventually arrive, _handleOnPresented and _handleOnDismissed find entry == null and return early — the builder's onPresented(nil, error) callback never fires.
Changing the error path to call result(true) instead of result(FlutterError(...)) would let both events be processed while the entry is still live, matching the behavior documented in BRIDGE-CONTRACT P0.4 and aligning with Android's event-only outcome delivery.
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/ios/Classes/PurchaselyV6Bridge.swift
Line: 272-299
Comment:
**P0.4 onPresented synthesis never fires on iOS display errors**
`events.emit(...)` wraps its payload in `DispatchQueue.main.async`, so the `onPresented` and `onDismissed` events are queued for a future run-loop iteration. But `result(FlutterError(...))` is called synchronously in the same closure and is processed first by Dart. The `_displayPresentation`/`_displayRequest` PlatformException handler removes the entry from `_entries` and completes the dismiss completer before either async event is delivered. When the events eventually arrive, `_handleOnPresented` and `_handleOnDismissed` find `entry == null` and return early — the builder's `onPresented(nil, error)` callback never fires.
Changing the error path to call `result(true)` instead of `result(FlutterError(...))` would let both events be processed while the entry is still live, matching the behavior documented in BRIDGE-CONTRACT P0.4 and aligning with Android's event-only outcome delivery.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…6.0.0 io.purchasely:core:6.0.0 is not yet on Maven Central/Google; resolve it from the local Maven repo for local builds, mirroring the Shaker sample. mavenLocal() is placed first in the plugin's rootProject.allprojects and the example app's allprojects repositories. To be removed once 6.0.0 is published. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…urface Presentation display, the action interceptor, and SDK init are now v6-only across Dart, iOS and Android. The legacy v5 paywall-display methods, the v5 action interceptor, Purchasely.start and the inline native view were removed; all other v5 methods (purchases, identity, attributes, products/plans, subscriptions data, events, offerings, consent, config) are kept and now require a PurchaselyBuilder start. Terminology: "paywall" -> "Presentation". - Dart: remove v5 presentation/interceptor/start + native_view_widget; keep the rest; rename paywall -> Presentation. - iOS: gut SwiftPurchaselyFlutterPlugin to a v6-only-presentation shell (keep register + kept v5 handlers + v5 event channels); delete NativeView(Factory) + presentation/interceptor ToMaps; fix PurchaselyV6Bridge for native 6.0. - Android: v6-only dispatch + ActivityAware; delete NativeView(Factory) + PLYProductActivity; port kept v5 methods to native core 6.0.0; fix v6 bridge (display import, PLYPresentationPlan.storeOfferId). - Tests: drop v5 presentation/interceptor tests, keep v6 + kept-v5 coverage. - Example: rewrite to v6-only init + presentation + interceptor. - Docs: add MIGRATION.md; update CHANGELOG/README/VERSIONS. BREAKING CHANGE: v5 presentation-display methods, the v5 action interceptor, Purchasely.start and the PLYPresentationView inline widget are removed. See purchasely/MIGRATION.md. presentSubscriptions is a no-op on Android (native 6.0 removed the screen). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…acy v5 surface" This reverts commit 2fb95b8.
…lugin, no v6 naming) This is a pure adaptation of the existing plugin to the Purchasely 6.0 native SDK — same public surface as before, just migrated. The separate presentation bridge added during the migration is removed and folded back into the one plugin per platform; there is no "v6" type/class/symbol anywhere. - Native: merge PurchaselyV6Bridge.swift / PurchaselyV6Bridge.kt INTO the single SwiftPurchaselyFlutterPlugin / PurchaselyFlutterPlugin. start, presentation (preload/display/close/back) and the action interceptor now call the 6.0 API (PLYPresentationBuilder/Request, interceptAction, ...); every other method is kept and adapted to 6.0 signature changes (allowDeeplink, handleDeeplink, isEligibleToOffer, storeOfferId, ...). Wire verbs are un-prefixed; the presentation/interceptor EventChannel is `purchasely-presentation-events`. - NativeView / NativeViewFactory kept and adapted to 6.0 (inline view built from a loaded presentation keyed by requestId). - Android: PLYProductActivity + PLYSubscriptionsActivity removed (the 6.0 Android SDK no longer exposes the subscriptions screen); presentSubscriptions is a no-op on Android (still works on iOS). - iOS: 3 dead v5 presentation/interceptor +ToMap extensions removed (their types changed/disappeared in 6.0); PLYPlan/Product/Subscription/OfferSignature +ToMap kept. - Dart: lib/src split kept; PurchaselyV6Bridge -> PurchaselyBridge, V6RunningMode/V6LogLevel -> RunningMode/LogLevel, verbs/channel de-"v6"'d; request_id.dart inlined; native_view_widget.dart + example presentation_screen kept and adapted; v6_demo_screen -> presentation_demo_screen. The Purchasely class drops only the old start/presentation/interceptor methods (now provided by the builders); all other methods kept. Verified: Android compileDebugKotlin OK, iOS simulator build OK, flutter analyze clean, 209 Dart tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the React Native SDK's migration docs for Flutter. - MIGRATION-v6.md: v5 → 6.0 guide (mapping table + before/after) — start, presentation and the action interceptor now use the 6.0 builder API; every other method is unchanged. Points at the Purchasely AI skills. - sdk_public_doc.md: public integration guide rewritten for the 6.0 API (PurchaselyBuilder, PresentationBuilder/Request, PresentationOutcome, PurchaselyBridge.registerInterceptor) with outcome + action-kind tables. - CHANGELOG.md: rewrite the 6.0.0-beta.0 entry to the real change set; drop the stale dual-façade wording and the non-existent Purchasely.interceptAction ref. - README.md (root + package): add the "Upgrading to 6.0?" link and fix stale V6RunningMode/V6LogLevel symbol names. - action_interceptor.dart: fix the doc comment to the real registration API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thin façade over PurchaselyBridge.ensureInstalled().registerInterceptor so the public way to register an action interceptor reads like the rest of the `Purchasely` API (mirrors the v5 `setPaywallActionInterceptorCallback` ergonomics): Purchasely.interceptAction(kind, handler) Purchasely.removeInterceptor(kind) Purchasely.removeAllInterceptors() The bridge API still works underneath. Docs (MIGRATION-v6.md, sdk_public_doc.md), the action_interceptor doc comment and the example now use the clean API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review of the 6.0 adaptation surfaced a real regression and several smaller
improvements.
- fix(start): the native `start` handlers (Android + iOS) still read the old v5
wire shape (`userId`, Int runningMode/logLevel, capitalized store names, Bool
storeKit1) while `PurchaselyBuilder.start()` sends the new shape (`appUserId`,
string `runningMode`/`logLevel`, lowercase stores, `storekitVersion`,
`allowDeeplink`/`allowCampaigns`). The mismatch silently dropped the user id,
forced Full mode (instead of the documented Observer default), never
registered a Store, and ignored deeplink/campaign flags. Both handlers now
read the builder contract; `getStoresInstances` matches lowercase
google/huawei/amazon. Added a bridge test asserting the exact `start` args.
- fix(leak): the per-requestId presentation maps (loaded/prepared/requests) were
never cleared; they are now removed in the `onDismissed` callback on both
platforms (matching the Dart side).
- docs: VERSIONS.md ("Flutter" not "React Native" + 6.0.0-beta.0 row); CHANGELOG
interceptor snippet uses Purchasely.interceptAction; podspec/build.gradle
comments reference the single plugin (no more "PurchaselyV6Bridge");
native_view_widget docstring no longer over-promises inline lifecycle.
- example: demonstrate Purchasely.interceptAction with a typed PurchasePayload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The embedded inline view previously rendered the screen but reported its
outcome on a dead `native_view` MethodChannel that Dart never handled, so the
PresentationRequest's onDismissed/outcome never fired for the inline path.
Both NativeViews now emit the same `{event:'onDismissed', requestId, outcome}`
envelope on the shared `purchasely-presentation-events` sink (identical shape to
the full-screen path). The Dart bridge already routes that by requestId — the
inline request is registered on preload() — so `PresentationRequest.onDismissed`
and the display()-style outcome now fire for inline presentations too.
- Android: NativeView calls PurchaselyFlutterPlugin.emitPresentationEvent with
the shared envelope/outcomeToMap; the dead native_view channel is removed;
the requestId is cleaned from the static maps on dismiss.
- iOS: NativeView emits via a static plugin helper, wiring the loaded
presentation's onDismissed plus a PLYEventDelegate `.presentationClosed`
fallback (the embedded child controller doesn't reliably fire the request
callback), guarded exactly-once; static maps cleaned on dismiss.
- Dart: native_view_widget docstring updated — inline now surfaces dismissal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adapts the Flutter plugin to the Purchasely 6.0 native SDK. This is a migration of the existing plugin — same public surface as before, just updated for 6.0 — not a rewrite. There is no separate "v6" module and no
v6/V6symbol anywhere: each platform keeps a singleFlutterPlugin, and the Dart code keeps thelib/src/split.What changed
start, presentation (preload/display/close/back) and the action interceptor now call the native 6.0 API. Their implementations were folded back into the single plugin per platform (SwiftPurchaselyFlutterPlugin.swift,PurchaselyFlutterPlugin.kt) — the temporaryPurchaselyV6Bridge.{swift,kt}is removed.allowDeeplink,handleDeeplink,isEligibleToOffer,storeOfferId,userLogout(true), …).NativeView/NativeViewFactory+native_view_widget.dart) kept and adapted to 6.0 (built from a loaded presentation keyed byrequestId).lib/src/split kept): dispatcher renamedPurchaselyV6Bridge→PurchaselyBridge;V6RunningMode/V6LogLevel→RunningMode/LogLevel; MethodChannel verbs un-prefixed; the presentation/interceptor stream isEventChannel('purchasely-presentation-events');request_id.dartinlined. The legacyPurchaselyclass drops only the old start/presentation/interceptor methods (now provided by the builders); the examplev6_demo_screen.dart→presentation_demo_screen.dart.Removed
PLYProductActivity+PLYSubscriptionsActivity— the 6.0 Android SDK no longer exposes the built-in subscriptions screen, sopresentSubscriptionsis a no-op on Android (still works on iOS).+ToMapextensions whose types changed/disappeared in 6.0 (PLYPlan/PLYProduct/PLYSubscription/PLYOfferSignature+ToMapare kept).Native SDK dependency status
io.purchasely:core:6.0.0, not yet on Maven Central; resolved frommavenLocal()for local builds.Purchasely 6.0.0, not yet on CocoaPods trunk (trunk tops out at 5.7.x); the 6.0 API lives on the iOS SDKdevelopbranch and is consumed locally via a:pathdev-pod. The example's iOS build config (Podfile dev-pod, deployment target 15.0) is intentionally kept local / uncommitted (machine-specific).mavenLocal()and the iOS:pathdev-pod once 6.0.0 ships.Test plan / verification
flutter analyze(package + example) — 0 errorsflutter test— 209 pass./gradlew :purchasely_flutter:compileDebugKotlin— BUILD SUCCESSFUL (againstcore:6.0.0from mavenLocal)flutter build ios --simulator— Built (against thePurchasely 6.0.0dev-pod)🤖 Generated with Claude Code