Skip to main content

Background

Get set up with the Helium SDK for Flutter. Reach out over your Helium slack channel or email founders@tryhelium.com for any questions.

Installation

Run flutter pub add helium_flutter or manually add the helium_flutter package to your pubspec.yaml:
dependencies:
  helium_flutter: ^3.0.5
Then run:
flutter pub get
The minimum version of Flutter supported by this SDK is 3.24.0.
Recommended - Make sure that Swift Package Manager support is enabled:
flutter upgrade
flutter config --enable-swift-package-manager
See this Flutter documentation for more details about Swift Package Manager.
You can still use Cocoapods for your dependencies if preferred. If you need to disable Swift Package Manager dependencies after having enabled it, refer to that same Flutter documentation.

iOS Settings

Helium requires iOS 15+. If your app already has a minimum of iOS 15 or higher, you’re all set. This can be specified in your ios/Podfile with:
platform :ios, '15.0'
If you still see errors related to minimum iOS version, consider updating to 15.0 or higher directly in the Xcode project.

Initialize Helium

In your app’s initialization code (typically in main.dart or your root widget), add the following to configure Helium and prepare your paywalls:
import 'package:helium_flutter/helium_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final heliumFlutter = HeliumFlutter();
  await heliumFlutter.initialize(
    apiKey: "<your-helium-api-key>",
  );

  runApp(const MyApp());
}
initialize
method
You can set a custom user ID and custom user traits with the initialize method or by calling:
await heliumFlutter.overrideUserId(
  newUserId: "your-custom-user-id",
  traits: {
    "exampleTrait": "value",
    "userType": "premium"
  }
);
In most cases there is no need to check download status. Helium will display a loading indication if a paywall is presented before download has completed.
After initialization, you can check the status of the paywalls download:
String downloadStatus = await heliumFlutter.getDownloadStatus() ?? 'Unknown';
The download status will be one of the following:
  • notDownloadedYet: The download has not been initiated.
  • inProgress: The download has not been initiated.
  • downloadSuccess: The download was successful.
  • downloadFailure: The download failed.
You can also check if paywalls have been downloaded with await heliumFlutter.paywallsLoaded()

Helium Events

To handle various paywall-related events, implement HeliumCallbacks and pass it to initialize:
abstract class HeliumCallbacks {
  Future<void> onPaywallEvent(HeliumPaywallEvent heliumPaywallEvent);
}
For example:
class LogCallbacks implements HeliumCallbacks {
  @override
  Future<void> onPaywallEvent(HeliumPaywallEvent heliumPaywallEvent) async {
    log('onPaywallEvent: ${heliumPaywallEvent.type} - trigger: ${heliumPaywallEvent.triggerName}');
  }
}

Handling Purchases

By default, Helium will handle purchase logic for you. If you want a custom implementation, you’ll want to implement HeliumPurchaseDelegate and pass that into initialize.
abstract class HeliumPurchaseDelegate {
  Future<HeliumPurchaseResult> makePurchase(String productId);

  Future<bool> restorePurchases();
}

Basic Implementation

Here’s a basic implementation of HeliumPurchaseDelegate:
import 'package:helium_flutter/helium_flutter.dart';
import 'dart:developer';

class PaymentCallbacks implements HeliumCallbacks {
  @override
  Future<HeliumPurchaseResult> makePurchase(String productId) async {
    log('makePurchase: $productId');
    // Implement your purchase logic here
    return HeliumPurchaseResult(status: HeliumTransactionStatus.purchased);
  }

  @override
  Future<bool> restorePurchases() async {
    log('restorePurchases called');
    // Implement your restore logic here
    return true;
  }
}

RevenueCat Implementation Example

import 'package:helium_flutter/helium_flutter.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'dart:developer';

class RevenueCatCallbacks implements HeliumCallbacks {
  @override
  Future<HeliumPurchaseResult> makePurchase(String productId) async {
    try {
      log('RevenueCat making purchase: $productId');
      final offerings = await Purchases.getOfferings();

      Package? packageToPurchase;

      // Search all offerings for package
      for (var offering in offerings.all.values) {
        for (var package in offering.availablePackages) {
          if (package.storeProduct.identifier == productId) {
            packageToPurchase = package;
            break;
          }
        }
        if (packageToPurchase != null) break;
      }

      if (packageToPurchase == null) {
        return HeliumPurchaseResult(
          status: HeliumTransactionStatus.failed,
          error: 'Product not found in any offering: $productId',
        );
      }

      final customerInfo = await Purchases.purchasePackage(packageToPurchase);

      // Check if the purchase was successful by looking at entitlements
      if (customerInfo.entitlements.active.isNotEmpty) {
        return HeliumPurchaseResult(status: HeliumTransactionStatus.purchased);
      } else {
        return HeliumPurchaseResult(status: HeliumTransactionStatus.failed);
      }
    } catch (e) {
      log('RevenueCat purchase error: $e');
      if (e is PurchasesErrorCode) {
        if (e == PurchasesErrorCode.purchaseCancelledError) {
          return HeliumPurchaseResult(status: HeliumTransactionStatus.cancelled);
        } else if (e == PurchasesErrorCode.paymentPendingError) {
          return HeliumPurchaseResult(status: HeliumTransactionStatus.pending);
        }
      }
      return HeliumPurchaseResult(
          status: HeliumTransactionStatus.failed,
          error: 'RevenueCat purchase error: ${(e as PlatformException?)?.message ?? "Unknown error"}'
      );
    }
  }

  @override
  Future<bool> restorePurchases() async {
    try {
      log('RevenueCat restoring purchases');
      final restoredInfo = await Purchases.restorePurchases();
      return restoredInfo.entitlements.active.isNotEmpty;
    } catch (e) {
      log('RevenueCat restore error: $e');
      return false;
    }
  }
}

Presenting Paywalls

There are two ways to show a Helium paywall in your Flutter app: You can present a paywall programmatically using the presentUpsell method:
ElevatedButton(
  onPressed: () {
    final heliumFlutter = HeliumFlutter();
    heliumFlutter.presentUpsell(context: context, trigger: 'insert-trigger-here');
  },
  child: Text('Show Premium Features'),
),
Configure triggers in the dashboard. You can also pass in custom paywall traits and event handlers directly to presentUpsell:
HeliumFlutter().presentUpsell(
  trigger: 'my_paywall',
  context: context,
  eventHandlers: PaywallEventHandlers(
    onOpen: (event) {
      log('${event.type} - trigger: ${event.triggerName}');
    },
    onClose: (event) {
      log('${event.type} - trigger: ${event.triggerName}');
    },
    onDismissed: (event) {
      log('${event.type} - trigger: ${event.triggerName}');
    },
    onPurchaseSucceeded: (event) {
      log('${event.type} - trigger: ${event.triggerName}');
    },
  ),
  customPaywallTraits: {
    "has_seen_intro_video": true,
  }
);

Via Widget Integration

You can also use the HeliumFlutter.getUpsellWidget method to embed a paywall directly in your widget tree:
class ExamplePageWithEmbeddedPaywall extends StatelessWidget {
  const ExamplePageWithEmbeddedPaywall({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: HeliumFlutter().getUpsellWidget(trigger: 'insert-trigger-here'),
    );
  }
}
You will have to handle your own dismissal in this case. You can do so by listening for the paywallDismissed event in onPaywallEvent of your HeliumCallbacks implementation.

Hiding Paywalls

To programmatically hide a paywall that was shown with the presentUpsell method:
bool hideResult = await heliumFlutter.hideUpsell() ?? false;

Checking Subscription Status & Entitlements

Use these methods to check current user subscription status.
/// Checks if the user has any active subscription (including non-renewable)
Future<bool> hasAnyActiveSubscription();

/// Checks if the user has any entitlement
Future<bool> hasAnyEntitlement();

Fallbacks

The Flutter SDK currently has two fallback options - 1) Fallback bundle and 2) a fallback view. Fallback situations should be quite rare, but to be safe, it is recommended you implement a fallback bundle or custom fallback view.
I