encore_flutter 2.0.1 copy "encore_flutter: ^2.0.1" to clipboard
encore_flutter: ^2.0.1 copied to clipboard

Flutter plugin wrapping the native Encore iOS and Android SDKs for monetization, offers, and entitlements. All offer UI is rendered natively via StoreKit (iOS) and Play Billing (Android).

Encore Flutter SDK #

Flutter plugin wrapping the native Encore iOS and Android SDKs. All offer UI is rendered natively — this plugin bridges configuration, identity, placement presentation, purchases, and outcome reporting via platform channels.

Installation #

Add to your pubspec.yaml:

dependencies:
  encore_flutter: ^2.0.0

iOS #

The EncoreKit CocoaPod is automatically included as a transitive dependency of the Flutter plugin — no manual pod configuration is needed.

Minimum deployment target: iOS 15.0.

Android #

The com.encorekit:encore AAR is included as a transitive dependency of the Flutter plugin — no manual Gradle configuration is needed.

Minimum SDK: 26.

Usage #

Configure #

Call once early in your app lifecycle (e.g. in main()):

import 'package:encore_flutter/encore_flutter.dart';

await Encore.shared.configure(
  apiKey: 'your_api_key',
  purchaseController: AppPurchases(),   // see below
  logLevel: EncoreLogLevel.debug,
);

Run purchases #

Encore controls when a purchase happens; your app owns how. Implement EncorePurchaseController and register the instance at configure — this is the only purchase path, and the SDK never runs purchase code you did not write. With no controller registered nothing is ever charged, and a presentation whose layout offers a product resolves EncorePublisherOutcome.notAttempted.

class AppPurchases implements EncorePurchaseController {
  @override
  Future<EncorePurchaseResult> purchase(EncorePurchaseRequest request) async {
    // request.productId / .placementId / .promoOfferId (iOS) / .basePlanId (Android)
    try {
      await mySubscriptionManager.purchase(request.productId);
      return EncorePurchaseResult.purchased;
    } on UserCancelled {
      return EncorePurchaseResult.cancelled;
    } on PurchaseDeferred {
      return EncorePurchaseResult.pending;   // Ask to Buy / SCA
    }
    // Throw for a real failure — Encore records it and resolves the
    // presentation with EncorePublisherOutcome.failed.
  }
}

The result is three-valued on purpose. pending is a deferred purchase (Ask to Buy, SCA, a pending Play transaction): the user has not abandoned the flow and has not been charged yet, so neither purchased nor cancelled is true, and the store's eventual webhook is the source of truth.

Registration can only happen at configure, because both native SDKs bind the controller once and expose no setter. It survives reset() — it is app-level infrastructure, not user state.

Identify User #

After authentication:

await Encore.shared.identify(
  userId: 'user_123',
  attributes: EncoreUserAttributes(
    email: 'user@example.com',
    subscriptionTier: 'premium',
  ),
);

Update Attributes #

await Encore.shared.setUserAttributes(
  EncoreUserAttributes(billingCycle: 'annual'),
);

Present Offers #

final result = await Encore.placement('cancel_flow').show();

switch (result) {
  case EncoreNotPresented(:final reason):
    print('Nothing shown: ${reason.name}');
  case EncorePresented(:final outcome):
    print('advertiser=${outcome.advertiser} '
          'publisher=${outcome.publisher.name} '
          'dismissal=${outcome.dismissal.name}');
}

show() never throws. Either nothing appeared — EncoreNotPresented, with a reason — or the interaction ran and EncorePresented carries the complete factual record.

That record has two independent funnels plus how the sheet ended:

Axis Type What it records
advertiser EncoreAdvertiserOutcome How far the Encore offer claim got: notAttempted / claimed / verified / cooldown / failed
publisher EncorePublisherOutcome What your purchase controller reported: notAttempted / purchased / cancelled / pending / failed
dismissal EncoreDismissReason How the sheet went away

The two funnels are independent — a single presentation can claim an offer and run a purchase. notAttempted is a real value ("funnel open, nothing entered it"), which is a different fact from null ("never presented at all").

There is deliberately no SDK-computed "unlocked" verdict: what a claim means is a property of the variant flow that served it, so any projection over app-global config could contradict the flow that actually ran. Branch on the raw axes:

final converted = result.claim != null ||
    result.publisher == EncorePublisherOutcome.purchased;

result.claim reads through both claimed and verified and gives you the EncoreClaimedOffercampaignId, advertiserName, and a nullable transactionId that joins this claim to the completion that lands days later (the same id the offer-completed webhook and the server-side sdk_offer_completed event carry). It is nullable because the transaction write can fail while the claim still happened; the claim is reported either way.

Observe outcomes #

Encore.shared.outcomes.listen((outcome) {
  switch (outcome) {
    case EncorePlacementPresentation(:final placementId, :final result):
      analytics.log('encore_presentation', placementId, result);
    case EncoreStrictUnlockVerified(:final transactionId):
      entitlements.refresh(transactionId);
  }
});

A broadcast Stream carrying every show() resolution — including the ones that presented nothing — plus strict-unlock verifications.

This is the observation channel; control flow belongs at the call site. Its reason for existing is EncoreStrictUnlockVerified, which resolves after the flow that produced it ended, possibly on a later launch, and so can never be a show() return value. It only fires under EncoreUnlockMode.strict. There is no replay — subscribe at startup if you want every outcome.

Use cases and copy #

To present the reward surface instead of the monetization sheet — a brand-funded reward at a moment the user has just accomplished something — set the use case and supply your own copy:

final result = await Encore.placement('streak_complete')
    .useCase(EncoreUseCase.rewardUsers)
    .headline('7 day streak!')
    .subheadline("Here's a little thank you from us")
    .show();

useCase defaults to EncoreUseCase.reduceChurn, so existing calls are unchanged. EncoreUseCase.rewardUsers is claim-only and never presents an in-app purchase.

Copy resolves in strict priority: the value you pass here, then the value configured in the Encore portal, then the shipped template default. A blank string is ignored, so the chain falls through rather than rendering an empty line.

headline / subheadline apply to both use cases — the native side writes them into the variable the active template reads.

If a use case resolves no layout of its own, the result is EncoreNotPresented(reason: EncoreNotPresentedReason.useCaseUnavailable) — typically because the use case is not enabled for this app. It is a correct no-op, not an error, and it never falls back to the monetization sheet: putting an IAP screen in front of a user at a moment they were meant to be rewarded is worse than showing nothing.

Claim button #

await Encore.shared.setClaimEnabled(false);   // gray out and disable

Reset (Logout) #

await Encore.shared.reset();

Migrating from 1.x #

2.0 tracks the native Encore 2.0 SDKs, which removed onPurchaseRequest entirely and replaced it with a registered purchase controller. There is no compatibility shim, because the only shim available would have had to collapse the three-valued purchase result back into 1.x's void/bool — which is exactly the bug 2.0 fixes.

1.x 2.0
Encore.shared.onPurchaseRequest(handler) Implement EncorePurchaseController, pass it to configure(purchaseController:)
Encore.shared.onPurchaseRequestResult(handler) Same — and return pending instead of false for deferred purchases
Encore.shared.onPurchaseComplete(handler) Removed. The SDK no longer runs purchases itself, so there is no native purchase to report — your controller already sees every purchase it runs
Encore.shared.onPassthrough(handler) Removed. Read the result instead: EncoreNotPresented means nothing was shown, and a presented record carries the dismissal and both funnels
Encore.shared.placements.setClaimEnabled(x) Encore.shared.setClaimEnabled(x)
EncorePresentationResultGranted EncorePresented + result.claim != null || result.publisher == EncorePublisherOutcome.purchased
EncorePresentationResultClaimed EncorePresented + result.claim != null
EncorePresentationResultNotGranted(reason: String) EncoreNotPresented(reason: EncoreNotPresentedReason) for "nothing shown", or EncorePresented + outcome.dismissal for "shown, then dismissed" — 1.x conflated the two
EncoreBillingPurchaseResult Removed with onPurchaseComplete
'use_case_unsupported' Gone. 1.x dropped the use case at the bridge, so rewardUsers could never present; 2.0 forwards it, leaving EncoreNotPresentedReason.useCaseUnavailable as the only unavailability reason

Before:

await Encore.shared.configure(apiKey: 'key', logLevel: EncoreLogLevel.debug);

Encore.shared.onPurchaseRequestResult((request) async {
  return await mySubscriptionManager.purchase(request.productId);
});
Encore.shared.onPassthrough((placementId) => runOriginalAction());

final result = await Encore.placement('cancel_flow').show();
if (result is EncorePresentationResultGranted) grantAccess();

After:

class AppPurchases implements EncorePurchaseController {
  @override
  Future<EncorePurchaseResult> purchase(EncorePurchaseRequest request) async {
    return await mySubscriptionManager.purchase(request.productId)
        ? EncorePurchaseResult.purchased
        : EncorePurchaseResult.cancelled;   // or .pending, when deferred
  }
}

await Encore.shared.configure(
  apiKey: 'key',
  purchaseController: AppPurchases(),
  logLevel: EncoreLogLevel.debug,
);

final result = await Encore.placement('cancel_flow').show();
if (result.claim != null ||
    result.publisher == EncorePublisherOutcome.purchased) {
  grantAccess();
} else {
  runOriginalAction();   // what onPassthrough used to do
}

Architecture #

Flutter App
    │
    ▼
┌──────────────────────────────────────┐
│  Encore Dart API                     │
│  (lib/src/encore.dart)               │
├──────────────────┬───────────────────┤
│  MethodChannel   │  EventChannel     │
│  com.encorekit/  │  com.encorekit/   │
│  encore          │  encore/outcomes  │
├──────────────────┼───────────────────┤
│  iOS Plugin      │  Android Plugin   │
│  (Swift)         │  (Kotlin)         │
├──────────────────┼───────────────────┤
│  EncoreKit       │  com.encorekit    │
│  CocoaPod        │  :encore AAR      │
└──────────────────┴───────────────────┘

The method channel carries request/response calls in both directions: the purchase controller is a reverse call, where native asks Dart to run the purchase and suspends until Dart answers. Dart cannot implement a Swift protocol or a Kotlin interface, so each plugin owns the native conformance and forwards over the channel.

Android's native controller is additionally handed the foreground Activity; the plugin absorbs it — it is valid only for the duration of the call and must not be retained, and iOS has no equivalent — so the Dart contract is identical on both platforms.

The event channel carries the outcomes stream, which is one-way and unbounded and so cannot ride a request/response method call. It bridges an AsyncStream on iOS and a SharedFlow on Android onto one Dart broadcast Stream.

API Reference #

Method Description
Encore.shared.configure(apiKey:, purchaseController:, logLevel:, unlock:) Initialize the SDK and register the purchase controller
Encore.shared.identify(userId:, attributes:) Associate user identity
Encore.shared.setUserAttributes(attributes) Merge user attributes
Encore.shared.reset() Clear user data (logout); the purchase controller survives
Encore.shared.setClaimEnabled(enabled) Enable/disable the claim CTA
Encore.shared.outcomes Stream<EncorePlacementOutcome> of every resolved outcome
Encore.placement(id).show() Present the native offer sheet; returns EncorePresentationResult
Encore.placement(id).useCase(useCase) Select what the placement should achieve: EncoreUseCase.reduceChurn (default) or .rewardUsers
Encore.placement(id).headline(text) Override the sheet headline, on every use case
Encore.placement(id).subheadline(text) Override the sheet subheadline, on every use case
1
likes
130
points
334
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter plugin wrapping the native Encore iOS and Android SDKs for monetization, offers, and entitlements. All offer UI is rendered natively via StoreKit (iOS) and Play Billing (Android).

Homepage
Repository (GitHub)
View/report issues

Topics

#monetization #in-app-purchase #subscriptions #offers

License

unknown (license)

Dependencies

flutter

More

Packages that depend on encore_flutter

Packages that implement encore_flutter