Migrating to v6 — Flutter
Migration guide for the Purchasely Flutter SDK from v5.x to v6.0.0
This guide covers the Flutter SDK (Dart). Version 6.0.0 adapts the Flutter plugin to the Purchasely 6.0 native SDKs (iOS Purchasely 6.0.0, Android io.purchasely:core 6.0.1). For the native layers this plugin bridges to, see the iOS guide or the Android guide, or the platform pages listed on the migration overview.
Three areas are breaking: starting the SDK, displaying / preloading / closing a presentation, and the action interceptor — they all move to a fluent builder API. Everything else on the Purchasely class (purchases, restore, identity, catalog, subscriptions, user attributes, events, dynamic offerings, consent and config) remains source-compatible except for the removed v5 aliases listed below. A paywall is now called a Presentation (or Screen).
Default running mode changed toobserverWith the 6.0 native SDKs the default
PLYRunningModeisPLYRunningMode.observer— the host app keeps control of the purchase flow unless it opts intofull. This change is silent: your code compiles without errors, but if you relied on the implicitfulldefault, your app will stop handling and validating purchases until you add.runningMode(PLYRunningMode.full). See SDK initialization.
Breaking type renames (v5 → v6)These v5 types were renamed or restructured — update all usages:
PresentPresentationResult→PLYPresentationOutcome,PLYPaywallAction→PLYPresentationActionKind,PLYPaywallInfo→PLYInterceptorInfo,PLYPaywallActionParameters→PLYActionPayload(+ typedPLY*Payloadsubclasses),PaywallActionInterceptorResult→ handler returningPLYInterceptResult.
Requirements
Pin all Purchasely packages to the exact same version. Mismatched versions cause runtime errors.
dependencies:
purchasely_flutter: 6.0.0
purchasely_google: 6.0.0 # required if you distribute on Google Play
purchasely_android_player: 6.0.0 # optional, video paywalls on AndroidHost build requirements:
| Platform | Requirement |
|---|---|
| iOS | minimum deployment target 13.4 |
| Android | minSdk 23, compileSdk 36 |
Native dependency6.0.0 targets the published Purchasely 6.0 native releases — Android
io.purchasely:core/google-play/player6.0.1on Maven Central, iOSPurchasely6.0.0on the CocoaPods trunk. The project builds from the public repositories with nomavenLocal()and no development pod.
Summary of breaking changes
| v5 | v6 |
|---|---|
Default running mode PLYRunningMode.full | Default running mode PLYRunningMode.observer ⚠️ |
Purchasely.start(apiKey: …) | Purchasely.apiKey('…').…start() (fluent builder) |
PLYRunningMode (4 values: full / observer / paywallObserver / transactionOnly) | PLYRunningMode (2 values: observer / full) |
Purchasely.fetchPresentation(...) | PLYPresentationBuilder.…build().preload() |
Purchasely.presentPresentationForPlacement(...) | PLYPresentationBuilder.placement(id).build().display([PLYTransition]) |
Purchasely.presentPresentationWithIdentifier(...) / presentProductWithIdentifier(...) / presentPlanWithIdentifier(...) | PLYPresentationBuilder.screen(id).…build().display([PLYTransition]) |
Purchasely.presentPresentation(presentation) | request.preload() then display() |
Purchasely.closePresentation() / hidePresentation() | presentation.close() |
Purchasely.showPresentation() | presentation.display() |
Purchasely.getPresentationView(...) | PLYPresentationView(request: …) widget |
PresentPresentationResult (display result) | PLYPresentationOutcome (5 fields, resolved at dismiss) |
setPaywallActionInterceptorCallback + onProcessAction(bool) | Purchasely.interceptAction(kind, handler) returning PLYInterceptResult |
PLYPaywallAction / PLYPaywallInfo / PLYPaywallActionParameters | PLYPresentationActionKind / PLYInterceptorInfo / PLYActionPayload (+ typed PLY*Payload) |
setDefaultPresentationResultHandler(cb) | Purchasely.setDefaultPresentationDismissHandler(cb) |
Transition(…, heightPercentage: x) | PLYTransition.drawer(height: PLYTransitionDimension.percentage(x)) — heightPercentage removed |
readyToOpenDeeplink(_) | allowDeeplink(_) (old name removed) |
isDeeplinkHandled(_) | handleDeeplink(_) (old name removed) |
synchronize() returning Future<void> (fire-and-forget) | synchronize() returning Future<bool>, throws on failure |
presentSubscriptions() | removed — build your own from userSubscriptions() |
displaySubscriptionCancellationInstruction() | removed |
Everything not in the table above keeps source-compatiblePurchasely.*signatures — see What stays the same.
1. Update dependencies & host build configuration
Pin the packages listed in Requirements, then:
flutter pub getOn Android, make sure the host app builds with compileSdk 36 and minSdk 23 (or higher). On iOS, raise the deployment target to 13.4 if needed (in ios/Podfile and the Xcode project), then run pod install --repo-update in ios/.
2. SDK initialization — fluent builder
Purchasely.start(apiKey: … runningMode: … storeKit1: … logLevel: … androidStores: … userId: …) is removed. Start with Purchasely.apiKey('…'), chain modifiers, finish with start(). start() returns a Future<bool> (true on success).
Before (v5)
import 'package:purchasely_flutter/purchasely_flutter.dart';
bool configured = await Purchasely.start(
apiKey: '<YOUR_API_KEY>',
androidStores: ['Google'],
storeKit1: false,
logLevel: PLYLogLevel.error,
runningMode: PLYRunningMode.full,
userId: 'user_id',
);
Purchasely.readyToOpenDeeplink(true); // removed in v6; use allowDeeplinkAfter (v6)
import 'package:purchasely_flutter/purchasely_flutter.dart';
final bool configured = await Purchasely.apiKey('<YOUR_API_KEY>')
.appUserId('user_id') // optional, defaults to anonymous
.runningMode(PLYRunningMode.full) // ← required for purchase handling & validation
.logLevel(PLYLogLevel.error) // debug | info | warn | error
.allowDeeplink(true) // allow the SDK to open deeplinks
.allowCampaigns(true) // optional campaign display gate
.stores([PLYStore.google]) // Android only: google | huawei | amazon
.storekitVersion(PLYStorekitVersion.storeKit2) // iOS only: storeKit2 (default) | storeKit1
.start();
Default running mode is nowPLYRunningMode.observerThe default
runningModechanged fromfulltoobserver. If you want Purchasely to handle and validate purchases, setPLYRunningMode.fullexplicitly. Inobservermode, the host app owns the purchase flow. The oldPLYRunningMode.transactionOnlyandPLYRunningMode.paywallObservervalues no longer exist — onlyobserverandfullremain.
Chain modifiers and defaults
| Modifier | Default | Notes |
|---|---|---|
appUserId(_) | null (anonymous) | |
runningMode(_) | PLYRunningMode.observer ⚠️ (was full in v5) | set PLYRunningMode.full to let Purchasely own the purchase flow |
logLevel(_) | PLYLogLevel.error | debug | info | warn | error |
stores([_]) | [PLYStore.google] | Android only: PLYStore.google | huawei | amazon |
storekitVersion(_) | PLYStorekitVersion.storeKit2 | iOS only: storeKit2 | storeKit1 (replaces storeKit1: bool) |
allowDeeplink(_) | unset (native SDK default) | can also be toggled later with Purchasely.allowDeeplink(bool) |
allowCampaigns(_) | true | campaign display gate |
3. Action interceptor — per-action API
setPaywallActionInterceptorCallback + onProcessAction(bool) are removed. Register one handler per action kind with Purchasely.interceptAction(kind, handler); the handler returns an explicit PLYInterceptResult (success / failed / notHandled) instead of calling onProcessAction(true/false).
Before (v5)
Purchasely.setPaywallActionInterceptorCallback((info, action, parameters, processAction) {
if (action == PLYPaywallAction.purchase) {
MyPurchaseSystem.purchase(parameters.plan.productId);
Purchasely.onProcessAction(false);
} else {
Purchasely.onProcessAction(true);
}
});After (v6)
import 'package:purchasely_flutter/purchasely_flutter.dart';
await Purchasely.interceptAction(
PLYPresentationActionKind.purchase,
(info, payload) async {
if (payload is PLYPurchasePayload) {
final ok = await MyPurchaseSystem.purchase(payload.plan.productId);
return ok ? PLYInterceptResult.success : PLYInterceptResult.failed;
}
return PLYInterceptResult.notHandled;
},
);
await Purchasely.interceptAction(
PLYPresentationActionKind.navigate,
(info, payload) async {
if (payload is PLYNavigatePayload) {
// open payload.url with your router / url_launcher
return PLYInterceptResult.success;
}
return PLYInterceptResult.notHandled;
},
);
// Cleanup
await Purchasely.removeActionInterceptor(PLYPresentationActionKind.purchase);
await Purchasely.removeAllActionInterceptors();Result semantics
PLYInterceptResult | Meaning | SDK behavior |
|---|---|---|
success | App handled the action successfully | Chain advances |
failed | App tried but failed | Remaining actions are skipped |
notHandled | App doesn't want to handle this | SDK executes the action itself |
Mapping from v5: onProcessAction(false) → PLYInterceptResult.success, onProcessAction(true) → PLYInterceptResult.notHandled.
Action kinds & payloads
Action kinds (PLYPresentationActionKind): close, closeAll, login, navigate, purchase, restore, openPresentation, openPlacement, promoCode, webCheckout. Each kind has a typed payload (PLYPurchasePayload, PLYNavigatePayload, PLYClosePayload, PLYCloseAllPayload, PLYOpenPresentationPayload, PLYOpenPlacementPayload, PLYWebCheckoutPayload); payload-less kinds (login, restore, promoCode) carry no extra fields. PLYPurchasePayload exposes real objects: plan is a PLYPlan, subscriptionOffer is a nullable PLYSubscriptionOffer, and offer is a nullable PLYPromoOffer.
Observer-mode bridgeIn
observermode, interceptpurchase/restore, run your own billing flow, then returnPLYInterceptResult.success(or.failed). On Android,PLYPurchasePayload.subscriptionOfferis a nullablePLYSubscriptionOffercarryingsubscriptionId,basePlanId,offerId, andofferToken.
4. Displaying, preloading & closing presentations
The Purchasely.presentPresentation* and fetchPresentation family are removed. Build a request with PLYPresentationBuilder (.placement(id), .screen(id), .contentId(id)), then .build() to get a PLYPresentationRequest with a lifecycle: preload() and display([PLYTransition]). display shows the screen and resolves at dismiss with a PLYPresentationOutcome.
Before (v5)
final result = await Purchasely.presentPresentationForPlacement(
'<YOUR_PLACEMENT_ID>',
contentId: 'my_content_id',
isFullscreen: true,
);
switch (result.result) {
case PLYPurchaseResult.purchased:
case PLYPurchaseResult.restored:
print('Purchased ${result.plan?.name}');
break;
case PLYPurchaseResult.cancelled:
break;
}After (v6)
final outcome = await PLYPresentationBuilder.placement('<YOUR_PLACEMENT_ID>')
.contentId('my_content_id')
.build()
.display(const PLYTransition.fullScreen());
// outcome: presentation, purchaseResult, plan, closeReason, error
if (outcome.error != null) {
print('Display error: ${outcome.error!.message}');
} else if (outcome.purchaseResult == PLYPurchaseResult.purchased ||
outcome.purchaseResult == PLYPurchaseResult.restored) {
print('Purchased ${outcome.plan?.name}');
} else {
print('Dismissed: ${outcome.closeReason}'); // button | backSystem | programmatic
}The PLYPresentationOutcome
PLYPresentationOutcomeThe old single-value display result is replaced by a 5-field PLYPresentationOutcome resolved at dismiss:
| Field | Type | Meaning |
|---|---|---|
presentation | PLYPresentation? | The displayed presentation (null if it never reached display) |
purchaseResult | PLYPurchaseResult? | purchased | restored | cancelled | null (no purchase action) |
plan | PLYPlan? | The purchased plan — fully typed, read outcome.plan?.vendorId, .name, .amount, … |
closeReason | PLYCloseReason? | button | backSystem | programmatic |
error | PLYPresentationError? | Display error (code, message) |
closeReasonparityBoth native 6.0 SDKs expose
closeReason, and Flutter surfaces it on both platforms (button/backSystem/programmatic). iOS maps itsinteractiveDismiss(swipe-down / nav-pop) tobackSystemto stay aligned with Android'sBACK_SYSTEM. The only field stillnullon iOS is the loaded presentationcontentId(PLYPresentationdoes not expose it on iOS); Android 6.0 reports it.
Plan offer fieldsAndroid 6.0 renamed introductory-price helpers to offer-price helpers. Flutter exposes the v6 names on
PLYPlan(hasOfferPrice,offerPrice,offerAmount,offerDuration,offerPeriod) and keeps the oldintro*fields populated as deprecated compatibility aliases.
Targeting a specific screen / product
// A specific presentation by screen id (was presentPresentationWithIdentifier)
await PLYPresentationBuilder.screen('SCREEN_ID').build().display(const PLYTransition.modal());
// A specific product / content inside a screen (was presentProductWithIdentifier)
await PLYPresentationBuilder.screen('SCREEN_ID').contentId('CONTENT_ID').build().display();Transitions — heightPercentage removed
heightPercentage removeddisplay([PLYTransition]) accepts an optional PLYTransition (replaces the old isFullscreen: bool). PLYTransition.heightPercentage was removed: drawer and popin transitions are now sized with PLYTransitionDimension, expressed as a percentage (0.0–1.0) or fixed pixel value. Leave a dimension null to size to content ("hug").
// Before (v5 / removed):
// Transition(type: TransitionType.drawer, heightPercentage: 0.5);
// After — factory constructors (preferred):
const PLYTransition.drawer(height: PLYTransitionDimension.percentage(0.5));
const PLYTransition.drawer(height: PLYTransitionDimension.pixel(300));
const PLYTransition.popin(
width: PLYTransitionDimension.pixel(320),
height: PLYTransitionDimension.percentage(0.6),
dismissible: false,
);
// After — explicit constructor (equivalent):
const PLYTransition(
type: PLYTransitionType.drawer,
height: PLYTransitionDimension.percentage(0.5),
);Available factory constructors on PLYTransition:
| Constructor | Description |
|---|---|
PLYTransition.fullScreen() | Full-screen (default) |
PLYTransition.modal({bool? dismissible}) | Modal sheet |
PLYTransition.push() | Push / navigation |
PLYTransition.drawer({PLYTransitionDimension? height, bool? dismissible, PLYTransitionColors? backgroundColors}) | Bottom drawer with optional height |
PLYTransition.popin({PLYTransitionDimension? width, PLYTransitionDimension? height, bool? dismissible, PLYTransitionColors? backgroundColors}) | Floating pop-in with optional dimensions |
Preloading (pre-fetch)
Purchasely.fetchPresentation(...) is removed. Build a PLYPresentationRequest, preload() it to fetch the screen from the network, then display() when ready (no extra network call).
Before (v5):
final presentation = await Purchasely.fetchPresentation(placementId: '<YOUR_PLACEMENT_ID>');
final result = await Purchasely.presentPresentation(presentation);After (v6) — Pattern A, separate preload and display (preload early, display later):
final request = PLYPresentationBuilder.placement('<YOUR_PLACEMENT_ID>').build();
final presentation = await request.preload(); // resolves when the screen is loaded
if (presentation.type == PLYPresentationType.deactivated) {
return; // No paywall to display for this placement
}
if (presentation.type == PLYPresentationType.client) {
return; // Display your own paywall (BYOS) — plan summaries are in presentation.plans
}
// Later, when ready to show it; resolves at dismiss
final outcome = await presentation.display(const PLYTransition.fullScreen());After (v6) — Pattern B, chained preload and display (one expression):
final outcome = await PLYPresentationBuilder.placement('<YOUR_PLACEMENT_ID>')
.build()
.preload()
.display(const PLYTransition.drawer(height: PLYTransitionDimension.percentage(0.5)));
preload()onPLYPresentationRequestreturnsFuture<PLYPresentation>. Thedisplay([PLYTransition?])method is available both onPLYPresentationdirectly (Pattern A) and via aFuture<PLYPresentation>extension (Pattern B).
Presentation types (PLYPresentationType): normal (default Purchasely paywall), fallback (requested one not found), deactivated (no paywall for this placement), client (your own paywall — BYOS). For a client presentation, Purchasely.clientPresentationDisplayed(presentation) and Purchasely.clientPresentationClosed(presentation) are kept with the same names — pass the PLYPresentation returned by preload() when you display/close your own paywall.
Presentation lifecycle (display / close / back)
The imperative Purchasely.showPresentation() / hidePresentation() / closePresentation() methods are removed — there is no global close anymore. Use the methods on the loaded PLYPresentation handle (from preload(), or from outcome.presentation):
final presentation = await PLYPresentationBuilder.placement('ONBOARDING').build().preload();
presentation.display(); // show (returns a future that resolves at dismiss)
presentation.close(); // dismiss programmatically (was Purchasely.closePresentation())
presentation.back(); // navigate back inside a multi-step (Flow) presentation5. Inline (embedded) presentations
Purchasely.getPresentationView(...) is removed. To render a presentation inline inside your widget tree, use the PLYPresentationView widget with a PLYPresentationRequest. The widget preloads the request and hands the result to the native inline view.
Before (v5)
final presentation = await Purchasely.fetchPresentation(placementId: 'onboarding');
// ...embed via Purchasely.getPresentationView(presentation: presentation, ...)After (v6)
import 'package:purchasely_flutter/native_view_widget.dart';
import 'package:purchasely_flutter/purchasely_flutter.dart';
final request = PLYPresentationBuilder.placement('onboarding')
.onDismissed((outcome) => print('inline dismissed: ${outcome.purchaseResult}'))
.build();
// In your build():
Expanded(
child: PLYPresentationView(
request: request,
loadingBuilder: const Center(child: CircularProgressIndicator()),
errorBuilder: (context, error) => Text('Error: ${error.message}'),
),
);6. Deeplinks, campaigns & default dismiss handler
The v5 deeplink methods are removed (they no longer compile):
| v5 (removed) | v6 |
|---|---|
Purchasely.readyToOpenDeeplink(_) | Purchasely.allowDeeplink(_) |
Purchasely.isDeeplinkHandled(_) | Purchasely.handleDeeplink(_) |
// Allow deeplinks and campaigns at start:
await Purchasely.apiKey('<YOUR_API_KEY>')
.allowDeeplink(true)
.allowCampaigns(true)
.start();
// These runtime gates are independent and can be toggled later:
await Purchasely.allowDeeplink(true);
await Purchasely.allowCampaigns(false);
// v6 deeplink handler:
final handled = await Purchasely.handleDeeplink('app://ply/presentations/');Default dismiss handler
Purchasely.setDefaultPresentationResultHandler(cb) is replaced by Purchasely.setDefaultPresentationDismissHandler(cb), which receives a PLYPresentationOutcome. It is the path for presentations opened by the SDK itself — campaigns, deeplinks, promoted in-app purchases — which have no host-side display() call to await.
await Purchasely.setDefaultPresentationDismissHandler((outcome) {
print('SDK presentation dismissed: ${outcome.presentation?.screenId} / '
'${outcome.purchaseResult} / ${outcome.closeReason}');
});Where the dismiss outcome is delivered (routing)
A dismissed presentation produces one PLYPresentationOutcome. There are three ways to receive it:
| Channel | What it is |
|---|---|
await display() | the return value — you await the call and get the outcome inline |
onDismissed | a per-presentation callback attached to this request/presentation |
setDefaultPresentationDismissHandler | a single global handler for the whole app |
Routing rule: at dismiss, the outcome goes to the onDismissed handler if one is set, otherwise to the global default handler. The deciding factor is the presence of onDismissed — not whether you awaited the future. Awaiting display() always gives you the outcome as a return value, but it does not by itself suppress the global handler.
await Purchasely.setDefaultPresentationDismissHandler((outcome) {
print('caught globally: ${outcome.purchaseResult}');
});
// (A) fire-and-forget, no onDismissed → the GLOBAL handler receives it.
PLYPresentationBuilder.placement('PLACEMENT').build().display();
// (B) local onDismissed set → the LOCAL handler receives it, global stays silent.
PLYPresentationBuilder.placement('PLACEMENT')
.onDismissed((outcome) => print('caught locally'))
.build()
.display();
// (C) await without onDismissed → the return value AND the global handler both
// receive it (set an onDismissed if you want the global to stay silent).
final outcome =
await PLYPresentationBuilder.placement('PLACEMENT').build().display();
// (D) await + onDismissed → return value + local handler receive it, global silent.
Rule of thumb: pick one channel per presentation — await it, or setonDismissed, or leave both off and let the global handler catch it.
7. synchronize() & removed APIs
synchronize() & removed APIssynchronize() now reports completion (breaking signature)
synchronize() now reports completion (breaking signature)The 6.0 native SDKs expose success/error callbacks on synchronize(). Purchasely.synchronize() now returns Future<bool> (was Future<void>): it resolves with true when the synchronization actually completes and throws a PlatformException on failure, instead of the previous fire-and-forget behaviour.
Before (v5)
Purchasely.synchronize(); // fire-and-forget, resolved immediatelyAfter (v6)
try {
final synced = await Purchasely.synchronize(); // true when sync completes
// Subscriptions cache is refreshed; safe to chain a subscriber-targeted presentation
} on PlatformException catch (e) {
print('Synchronize failed: ${e.message}');
}Removed: native subscriptions & cancellation survey UI
The built-in subscription management and cancellation survey UI was removed from the 6.0 native SDKs on both platforms:
Purchasely.presentSubscriptions()is removed entirely from the Flutter API (Dart, iOS, Android). It is no longer a no-op — the method no longer exists. There is no drop-in replacement.Purchasely.displaySubscriptionCancellationInstruction()is removed too.
Build your own subscriptions screen from the data APIs that remain:
final active = await Purchasely.userSubscriptions(); // active subscriptions
final history = await Purchasely.userSubscriptionsHistory(); // expired subscriptionsWhat stays the same
Only the paywall surface (start, display / preload / close / back, the action interceptor, default dismiss handler) has breaking API changes. Every other Purchasely.* method remains source-compatible except the removed v5 aliases:
- Purchases:
purchaseWithPlanVendorId,signPromotionalOffer. - Restore:
restoreAllProducts,silentRestoreAllProducts,userDidConsumeSubscriptionContent. - Identity:
userLogin,userLogout,isAnonymous,anonymousUserId. - Catalog:
allProducts,productWithIdentifier,planWithIdentifier,isEligibleForIntroOffer. - Subscriptions data:
userSubscriptions,userSubscriptionsHistory. - User attributes:
setUserAttributeWithString/WithInt/WithDouble/WithBoolean/WithDate/WithStringArray/WithIntArray/WithDoubleArray/WithBooleanArray,incrementUserAttribute,decrementUserAttribute,userAttribute,userAttributes,clearUserAttribute,clearUserAttributes,clearBuiltInAttributes,setAttribute,setUserAttributeListener/clearUserAttributeListener. - Events:
listenToEvents/stopListeningToEvents,listenToPurchases/stopListeningToPurchases. - Dynamic offerings:
setDynamicOffering,getDynamicOfferings,removeDynamicOffering,clearDynamicOfferings. - Consent:
revokeDataProcessingConsent. - Config / misc:
setLanguage,setThemeMode,setLogLevel,synchronize(new signature — see above),allowDeeplink,allowCampaigns,handleDeeplink,setDebugMode.
New in v6: Apple commitment plans (iOS 26.4+)
v6 surfaces Apple's "monthly subscription with N-month commitment" (installment) billing. This is Apple-only: on Android and other platforms the fields below are always empty / null, so guard on them before use.
PLYPlan.commitmentInfo—List<PLYCommitmentInfo>(empty when the plan has no commitment). Populated wherever a plan is exposed:allProducts,planWithIdentifier, thepurchaseinterceptor payload, and the presentation outcome plan. EachPLYCommitmentInfocarries:billingPlanType(PLYBillingPlanType:unspecified/upFront/monthly),billingPrice(double?),billingPeriod(ISO 8601 duration, e.g."P1M"),totalPrice(double?),totalPeriod(e.g."P1Y"),totalDuration(int?, number of billing cycles).PLYSubscription.commitmentProgress—PLYCommitmentProgress?onuserSubscriptions()/userSubscriptionsHistory()results:billingPeriodNumber(int?),totalBillingPeriods(int?),commitmentExpiresDate(ISO 8601String?),commitmentPrice(double?).PLYDynamicOfferinggains an optionalbillingPlanType(PLYBillingPlanType, defaults tounspecified) to force a commitment plan type when callingsetDynamicOffering.
final plan = await Purchasely.planWithIdentifier('my_plan');
for (final c in plan?.commitmentInfo ?? const []) {
print('${c.billingPlanType}: ${c.billingPrice} every ${c.billingPeriod}, '
'total ${c.totalPrice} over ${c.totalDuration} cycles');
}
// Force the monthly-commitment variant of a plan in a placement:
await Purchasely.setDynamicOffering(
PLYDynamicOffering('ref', 'my_plan', null, PLYBillingPlanType.monthly),
);Removed APIs
| v5 API (removed) | v6 replacement |
|---|---|
Purchasely.start(apiKey: …, androidStores: …, storeKit1: …, logLevel: …, runningMode: …, userId: …) | Purchasely.apiKey('…').appUserId(userId).runningMode(PLYRunningMode.full).logLevel(PLYLogLevel.error).stores([PLYStore.google]).storekitVersion(PLYStorekitVersion.storeKit2).start() |
Purchasely.fetchPresentation(placementId: id) | PLYPresentationBuilder.placement(id).build().preload() |
Purchasely.presentPresentationForPlacement(id, isFullscreen: …) | PLYPresentationBuilder.placement(id).build().display(const PLYTransition.fullScreen()) |
Purchasely.presentPresentationWithIdentifier(presentationId, …) | PLYPresentationBuilder.screen(id).build().display(const PLYTransition.modal()) |
Purchasely.presentPresentation(presentation) | preload then display the same request: final req = PLYPresentationBuilder.placement(id).build(); await req.preload(); await req.display(); |
Purchasely.presentProductWithIdentifier(productId, …) | PLYPresentationBuilder.screen(id).contentId(contentId).build().display() |
Purchasely.presentPlanWithIdentifier(planId, …) | PLYPresentationBuilder.screen(id).build().display() |
Purchasely.getPresentationView(...) | the PLYPresentationView(request: …) widget |
Purchasely.closePresentation() / hidePresentation() | presentation.close() (on the loaded PLYPresentation) |
Purchasely.showPresentation() | presentation.display() (on the loaded PLYPresentation) |
Purchasely.setDefaultPresentationResultHandler(cb) / setDefaultPresentationResultCallback(cb) | Purchasely.setDefaultPresentationDismissHandler((outcome) => …) |
Purchasely.setPaywallActionInterceptorCallback(cb) + Purchasely.onProcessAction(bool) | Purchasely.interceptAction(kind, handler) returning PLYInterceptResult |
Purchasely.readyToOpenDeeplink(bool) | Purchasely.allowDeeplink(bool) |
Purchasely.isDeeplinkHandled(deeplink) | Purchasely.handleDeeplink(deeplink) |
Purchasely.presentSubscriptions() | none — build your own from userSubscriptions() / userSubscriptionsHistory() |
Purchasely.displaySubscriptionCancellationInstruction() | none |
PLYRunningMode.transactionOnly / PLYRunningMode.paywallObserver | none — only observer and full remain |
Transition.heightPercentage | PLYTransitionDimension.percentage(…) / .pixel(…) on PLYTransition.drawer / .popin |
Purchasely.clientPresentationDisplayed(presentation)andPurchasely.clientPresentationClosed(presentation)are kept with the same names — pass thePLYPresentationreturned bypreload()(typePLYPresentationType.client).
Migration checklist
- Pin
purchasely_flutter/purchasely_google/purchasely_android_playerto6.0.0and runflutter pub get - Bump host builds: Android
minSdk 23/compileSdk 36, iOS deployment target13.4 - Replace
Purchasely.start(apiKey: …)withPurchasely.apiKey('…').…start() - If you use Full mode, add explicit
.runningMode(PLYRunningMode.full)— the default changed toobserver⚠️ - Remove
PLYRunningMode.transactionOnly/PLYRunningMode.paywallObserver— onlyobserverandfullremain - Replace
storeKit1: boolwith.storekitVersion(PLYStorekitVersion.…)andandroidStores: [...]with.stores([PLYStore.…]) - Rename v5 types:
PresentPresentationResult→PLYPresentationOutcome,PLYPaywallAction→PLYPresentationActionKind,PLYPaywallInfo→PLYInterceptorInfo,PLYPaywallActionParameters→PLYActionPayload - Replace
setPaywallActionInterceptorCallback+onProcessActionwith per-kindPurchasely.interceptAction(kind, handler)returningPLYInterceptResult - Replace
presentPresentationForPlacement/presentPresentationWithIdentifier/presentProductWithIdentifier/presentPlanWithIdentifier/fetchPresentationwithPLYPresentationBuilder.…build().display()/.preload() - Read display results from the 5-field
PLYPresentationOutcome(resolved at dismiss) - Replace
closePresentation()/hidePresentation()/showPresentation()withpresentation.close()/presentation.display() - Replace
Transition(…, heightPercentage: x)withPLYTransition.drawer(height: PLYTransitionDimension.percentage(x)) - Replace
getPresentationView(...)with thePLYPresentationView(request: …)widget - Replace
readyToOpenDeeplink(bool)withallowDeeplink(bool)andisDeeplinkHandled(uri)withhandleDeeplink(uri)(v5 names removed) - Replace
setDefaultPresentationResultHandler(cb)withsetDefaultPresentationDismissHandler(cb)and check the outcome routing (await/onDismissed/ global) - Update
synchronize()call sites: it now returnsFuture<bool>and throws aPlatformExceptionon failure - Remove
presentSubscriptions()/displaySubscriptionCancellationInstruction()— build your own UI fromuserSubscriptions()/userSubscriptionsHistory() - Migrate
intro*plan helpers to theoffer*equivalents (intro*kept as deprecated aliases) - Verify: init resolves
true, a placement and a screen presentation display, the outcome carries the expectedpurchaseResult/closeReason, and in Observer mode yourpurchase/restoreinterceptors resolve aPLYInterceptResultexactly once
Need a hand?
The Purchasely AI plugin and the purchasely-integrate, purchasely-review and purchasely-debug skills can scan your project and rewrite the old paywall calls to the new builder API. Point them at the files that call Purchasely.start(...), presentPresentationForPlacement(...), fetchPresentation(...), setPaywallActionInterceptorCallback(...), etc.
Updated 12 days ago

