encore_flutter 2.3.1 copy "encore_flutter: ^2.3.1" to clipboard
encore_flutter: ^2.3.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).

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show PlatformException;
import 'package:encore_flutter/encore_flutter.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Encore Flutter Example',
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<String> _eventLog = [];
  String? _activeLanguage;
  StreamSubscription<EncorePlacementOutcome>? _outcomes;

  @override
  void initState() {
    super.initState();
    _initEncore();
  }

  @override
  void dispose() {
    _outcomes?.cancel();
    super.dispose();
  }

  void _log(String message) {
    final now = DateTime.now();
    final timestamp =
        '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
    setState(() => _eventLog.insert(0, '[$timestamp] $message'));
  }

  Future<void> _initEncore() async {
    await Purchases.configure(
      PurchasesConfiguration('YOUR_REVENUECAT_API_KEY'),
    );

    // The purchase controller is registered here and nowhere else — the
    // native SDKs bind it once, inside configure, and it survives reset().
    await Encore.shared.configure(
      apiKey: 'pk_test_dyzxq1bl6jdt9dly78qdjnad',
      purchaseController: RevenueCatPurchaseController(_log),
      logLevel: EncoreLogLevel.debug,
    );

    // Passive observation of every outcome, including strict-unlock
    // verifications that land after (or on a later launch than) the flow.
    _outcomes = Encore.shared.outcomes.listen((outcome) {
      switch (outcome) {
        case EncorePlacementPresentation(:final placementId, :final result):
          _log('outcome: $placementId → $result');
        case EncoreStrictUnlockVerified(:final transactionId):
          _log('outcome: strict unlock verified ($transactionId)');
      }
    });

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

    _log('identify: demo_user_flutter_001');
  }

  Future<void> _showOffer() async {
    _log('placement: presenting...');
    final result = await Encore.placement('example_placement').show();
    _logResult(result);
  }

  /// Reward for trying lives on reward-users placements: that is where a
  /// reward-for-trying variant can be served. Passing the prize commits the
  /// app to granting it when `userConfirmedCompletion` comes back true.
  Future<void> _showRewardOffer() async {
    _log('reward placement: presenting...');
    final result = await Encore.placement('example_reward_placement')
        .useCase(EncoreUseCase.rewardUsers)
        .publisherReward(const EncorePublisherReward(
          id: 'coins_100',
          title: '100 coins',
          detail: 'Added to your wallet',
          iconUrl: 'https://example.com/coin.png',
        ))
        .show();
    _logResult(result);

    // The user said they finished. Their word, not a verified conversion;
    // grant your own prize here, from the show() result only.
    if (result.claim?.userConfirmedCompletion == true) {
      _log('reward placement: user confirmed completion, grant 100 coins');
    }
  }

  void _logResult(EncorePresentationResult result) {
    // The record carries facts, not a verdict: branch on the raw axes.
    final message = switch (result) {
      EncoreNotPresented(:final reason) =>
        'placement: nothing shown (${reason.name})',
      EncorePresented(:final outcome) => 'placement: '
          'advertiser=${outcome.advertiser} '
          'publisher=${outcome.publisher.name} '
          'dismissal=${outcome.dismissal.name}',
    };
    _log(message);

    final converted = result.claim != null ||
        result.publisher == EncorePublisherOutcome.purchased;
    _log(converted ? 'placement: converted' : 'placement: not converted');
  }

  Future<void> _resetSdk() async {
    await Encore.shared.reset();
    // The language is a device preference, so reset (logout) keeps it and
    // the picker keeps showing it. Only clearLanguage() removes it.
    _log('reset: SUCCESS (language kept: ${_activeLanguage ?? 'device'})');
  }

  /// Show Encore's copy in [code]'s language instead of the device's, from
  /// the next config and offers fetch. This is how to test localization
  /// without changing the device language in Settings.
  ///
  /// Pass `null` to clear and revert to device locale through
  /// `clearLanguage()`.
  Future<void> _setLanguage(String? code) async {
    if (code == null) {
      await Encore.shared.clearLanguage();
    } else {
      await Encore.shared.setLanguage(code);
    }
    setState(() => _activeLanguage = code);
    _log(code == null
        ? 'clearLanguage: language cleared (device locale)'
        : 'setLanguage: $code → next placement should render in this locale');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Encore Flutter Example')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Row(
                  children: [
                    Expanded(
                      child: FilledButton(
                        onPressed: _showOffer,
                        child: const Text('Show Offer'),
                      ),
                    ),
                    const SizedBox(width: 12),
                    Expanded(
                      child: OutlinedButton(
                        onPressed: _resetSdk,
                        child: const Text('Reset SDK'),
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                FilledButton.tonal(
                  onPressed: _showRewardOffer,
                  child: const Text('Show Reward Offer (reward for trying)'),
                ),
                const SizedBox(height: 12),
                _LanguagePicker(
                  active: _activeLanguage,
                  onPick: _setLanguage,
                ),
              ],
            ),
          ),
          const Divider(height: 1),
          Expanded(
            child: _eventLog.isEmpty
                ? const Center(child: Text('No events yet'))
                : ListView.builder(
                    padding: const EdgeInsets.all(12),
                    itemCount: _eventLog.length,
                    itemBuilder: (context, index) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        _eventLog[index],
                        style: Theme.of(context).textTheme.bodySmall?.copyWith(
                              fontFamily: 'monospace',
                            ),
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }
}

/// Quick language picker for end-to-end testing of config and offers
/// localization. Tap a language and the next placement renders in it, when
/// the app has that language enabled with approved translations. "Auto"
/// clears the choice so copy follows the device language.
class _LanguagePicker extends StatelessWidget {
  const _LanguagePicker({required this.active, required this.onPick});

  final String? active;
  final Future<void> Function(String?) onPick;

  static const _options = <(String code, String label)>[
    ('en', '🇺🇸 EN'),
    ('de', '🇩🇪 DE'),
    ('fr', '🇫🇷 FR'),
    ('es', '🇪🇸 ES'),
    ('it', '🇮🇹 IT'),
    ('pl', '🇵🇱 PL'),
    ('pt', '🇧🇷 PT'),
  ];

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 6,
      runSpacing: 6,
      crossAxisAlignment: WrapCrossAlignment.center,
      children: [
        const Text('Language:', style: TextStyle(fontWeight: FontWeight.w500)),
        for (final (code, label) in _options)
          ChoiceChip(
            label: Text(label),
            selected: active == code,
            onSelected: (_) => onPick(code),
          ),
        TextButton(
          onPressed: active == null ? null : () => onPick(null),
          child: const Text('Auto'),
        ),
      ],
    );
  }
}

/// Routes Encore's purchase requests through RevenueCat.
///
/// The three-valued return is the point: RevenueCat surfaces a deferred
/// purchase (Ask to Buy, SCA) as `paymentPendingError`, which is neither a
/// completed purchase nor an abandonment. Reporting it as either one would
/// misstate the funnel, so it maps to its own value.
class RevenueCatPurchaseController implements EncorePurchaseController {
  RevenueCatPurchaseController(this._log);

  final void Function(String) _log;

  @override
  Future<EncorePurchaseResult> purchase(EncorePurchaseRequest request) async {
    _log('purchase: productId=${request.productId}, '
        'placementId=${request.placementId}');

    final products = await Purchases.getProducts([request.productId]);
    if (products.isEmpty) {
      // A real failure, not a cancellation \u2014 throw so Encore records it.
      throw StateError('No RevenueCat product for ${request.productId}');
    }

    try {
      await Purchases.purchaseStoreProduct(products.first);
      _log('purchase: purchased');
      return EncorePurchaseResult.purchased;
    } on PlatformException catch (e) {
      switch (PurchasesErrorHelper.getErrorCode(e)) {
        case PurchasesErrorCode.purchaseCancelledError:
          _log('purchase: cancelled');
          return EncorePurchaseResult.cancelled;
        case PurchasesErrorCode.paymentPendingError:
          _log('purchase: pending');
          return EncorePurchaseResult.pending;
        default:
          _log('purchase: failed (${e.message})');
          rethrow;
      }
    }
  }
}
1
likes
130
points
623
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