> ## Documentation Index
> Fetch the complete documentation index at: https://helium-figma-import-quickguide.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Quickstart (Flutter)

> Integrate Helium into your Flutter App

## Background

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

<Warning>
  The Helium Flutter SDK **currently supports iOS only. Android support is expected mid- to late-November.**
</Warning>

## Installation

<Info>
  The minimum version of Flutter supported by this SDK is **3.24.0**.
</Info>

To add the Helium package, run:

```bash theme={null}
flutter pub add helium_flutter
```

Or if you prefer, you can add the helium\_flutter package to your pubspec.yaml:

```yaml theme={null}
dependencies:
  helium_flutter: ^3.0.12
```

then run:

```bash theme={null}
flutter pub get
```

<Tip>
  **Recommended -**  Make sure that Swift Package Manager support is enabled:
</Tip>

```bash theme={null}
flutter upgrade
flutter config --enable-swift-package-manager
```

See this [Flutter documentation](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers) for more details about Swift Package Manager.

<Info>
  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](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers#how-to-turn-off-swift-package-manager).
</Info>

### 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](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers#how-to-use-a-swift-package-manager-flutter-plugin-that-requires-a-higher-os-version).

## 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:

```dart theme={null}
import 'package:helium_flutter/helium_flutter.dart';

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

  final heliumFlutter = HeliumFlutter();
  await heliumFlutter.initialize(
    apiKey: "<your-helium-api-key>",
//    fallbackPaywall: Text("your fallback view"),
//    fallbackBundleAssetPath: "fallback-bundle-xxxx-xx-xx.json",
  );

  runApp(const MyApp());
}
```

<Tip>
  Including a fallback option is highly recommended. See the Fallbacks section on this page.
</Tip>

<ResponseField name="initialize" type="method">
  <Expandable title="parameters">
    <ResponseField name="apiKey" type="string" required>
      You can create or retrieve your api key from [Account Settings](https://app.tryhelium.com/profile).
    </ResponseField>

    <ResponseField name="callbacks" type="HeliumCallbacks?">
      *(Optional)* Pass in a listener for paywall events.
    </ResponseField>

    <ResponseField name="purchaseDelegate" type="HeliumPurchaseDelegate?">
      *(Optional)* Delegate for handling your own purchase logic. If not provided, Helium will handle purchase logic for you.
    </ResponseField>

    <ResponseField name="fallbackPaywall" type="Widget?">
      *(Optional)* A widget to display if paywall fails to display. See Fallbacks section.
    </ResponseField>

    <ResponseField name="customUserId" type="String?">
      *(Optional)* If set, a custom user id to use instead of Helium's. We'll use this id when forwarding to third party analytics services, so this can be used for attribution (e.g. an amplitude user id, or a custom user id from your own analytics service)
    </ResponseField>

    <ResponseField name="customUserTraits" type="Map<String, dynamic>?">
      *(Optional)* Pass in custom user traits to be used for targeting, personalization, and dynamic paywall content.
    </ResponseField>

    <ResponseField name="revenueCatAppUserId" type="String?">
      *(Optional)* RevenueCat ONLY. Supply RevenueCat appUserID here (and initialize RevenueCat before Helium initialize). You can also set this value outside of initialize: `HeliumFlutter().setRevenueCatAppUserId(await Purchases.appUserID)`
    </ResponseField>

    <ResponseField name="fallbackBundleAssetPath" type="String?">
      *(Optional)* Path to fallback bundle. See Fallbacks section.
    </ResponseField>

    <ResponseField name="paywallLoadingConfig" type="HeliumPaywallLoadingConfig?">
      *(Optional)* Set loading budget in seconds and whether to even show a loading state. This can be done per trigger.

      ```dart theme={null}
      final paywallLoadingConfig = HeliumPaywallLoadingConfig(
        loadingBudget: 5, // after 5 seconds, show fallback instead
        perTriggerLoadingConfig: {
          "onboarding": TriggerLoadingConfig(useLoadingState: false),
          "from_menu": TriggerLoadingConfig(useLoadingState: true, loadingBudget: 2),
        }
      );
      ```
    </ResponseField>
  </Expandable>
</ResponseField>

<Accordion title="Custom User ID and Custom User Traits">
  You can set a custom user ID and custom user traits with the `initialize` method or by calling:

  ```dart theme={null}
  await heliumFlutter.overrideUserId(
    newUserId: "your-custom-user-id",
    traits: {
      "exampleTrait": "value",
      "userType": "premium"
    }
  );
  ```

  Set these values before or during `initialize` to ensure consistency in analytics events and for the best experimentation results.
</Accordion>

<Accordion title="Checking Download Status">
  <Note>
    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.
  </Note>

  After initialization, you can check the status of the paywalls download:

  ```dart theme={null}
  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()`
</Accordion>

## Presenting Paywalls

<Note>
  You must have a trigger and workflow configured in the [dashboard](https://app.tryhelium.com/workflows) in order to show a paywall.
</Note>

You can present a paywall programmatically using the `presentUpsell` method:

```dart theme={null}
ElevatedButton(
  onPressed: () {
    final heliumFlutter = HeliumFlutter();
    heliumFlutter.presentUpsell(context: context, trigger: 'insert-trigger-here');
  },
  child: Text('Show Premium Features'),
),
```

<Warning>
  Do not call `presentUpsell` in `Widget build()` as this can have unpredictable behavior.
</Warning>

You can also pass in event handlers and custom paywall traits directly to `presentUpsell`:

```dart theme={null}
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}');
    },
    onAnyEvent: (event) {
      // A handler for all paywall-related events.
      // Note that if you have other handlers (i.e. onOpen) set up,
      // both that handler AND this one will fire during paywall open.
    },
  ),
  customPaywallTraits: {
    "has_seen_intro_video": true,
  }
);
```

<Info>
  Looking for alternative presentation methods? Check out the guide on [Ways to Show a Paywall](/guides/ways-to-show-paywall).
</Info>

You should now be able to see Helium paywalls in your app! Well done! 🎉

## Helium Events

To handle various [paywall-related events](/sdk/helium-events), implement `HeliumCallbacks` and pass it to initialize:

```dart theme={null}
abstract class HeliumCallbacks {
  Future<void> onPaywallEvent(HeliumPaywallEvent heliumPaywallEvent);
}
```

For example:

```dart theme={null}
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`.

```dart theme={null}
abstract class HeliumPurchaseDelegate {
  Future<HeliumPurchaseResult> makePurchase(String productId);

  Future<bool> restorePurchases();
}
```

#### Basic Implementation

Here's a basic (incomplete) implementation of `HeliumPurchaseDelegate`:

```dart theme={null}
import 'package:helium_flutter/helium_flutter.dart';
import 'dart:developer';

class CustomPurchaseDelegate implements HeliumPurchaseDelegate {
  @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

```dart theme={null}
import 'package:helium_flutter/helium_flutter.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'dart:developer';

class RevenueCatPurchaseDelegate implements HeliumPurchaseDelegate {
  @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;
      }

      CustomerInfo? customerInfo;
      if (packageToPurchase == null) {
        final fetchedProducts = await Purchases.getProducts([productId]);
        if (fetchedProducts.isEmpty) {
          return HeliumPurchaseResult(
              status: HeliumTransactionStatus.failed,
              error: 'Product not found in any offering and could not be retrieved: $productId'
          );
        } else {
          customerInfo = await Purchases.purchaseStoreProduct(fetchedProducts.first);
        }
      } else {
        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;
    }
  }
}
```

## Checking Subscription Status & Entitlements

Use these methods to check current user subscription status.

```dart theme={null}
/// Checks if the user has any active subscription (including non-renewable)
Future<bool> hasAnyActiveSubscription();

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

/// Checks if the user has an active entitlement for any product attached to the paywall that will show for provided trigger.
/// - Parameter trigger: Trigger that would be used to show the paywall.
/// - Returns: `true` if the user has bought one of the products on the paywall.
///            `false` if not. Returns `null` if not known (i.e. the paywall is not downloaded yet).
Future<bool?> hasEntitlementForPaywall(String trigger) =>
    HeliumFlutterPlatform.instance.hasEntitlementForPaywall(trigger);
```

#### Example Usage

<Tip>
  Check entitlements before showing paywalls to avoid showing a paywall to a user who should not see it.
</Tip>

<CodeGroup>
  ```dart Use dontShowIfAlreadyEntitled with presentUpsell theme={null}
  heliumFlutter.presentUpsell(
    trigger: 'my_paywall',
    context: context,
    dontShowIfAlreadyEntitled: true
  );
  ```

  ```dart Check before showing paywall theme={null}
  final heliumFlutter = HeliumFlutter();
  final hasActiveSubscription = await heliumFlutter.hasAnyActiveSubscription();
  if (hasActiveSubscription) {
    featureGatedLogic(); // replace this with your logic
  } else {
    heliumFlutter.presentUpsell(
      trigger: 'my_paywall',
      context: context,
      eventHandlers: PaywallEventHandlers(
        onPurchaseSucceeded: (event) {
          featureGatedLogic(); // replace this with your logic
        },
      )
    );
  }
  ```
</CodeGroup>

## Fallbacks

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