---
title: Flutter – MomentPerks Prefetch Integration Guide
slug: flutter-momentperks-prefetch-integration-guide
icon: {"faIcon":"fa-solid fa-toolbox"}
docTags: 
createdAt: 2025-07-26T11:13:13.608Z
---

# Overview

This guide explains how to integrate the MomentScience Moments solution into your Flutter application to present personalized offers during the checkout experience.

This guide covers two integration modes, both designed to preload and display offers in a WebView:

- SDK Prefetch Mode
- API Prefetch Mode

Both options enable a high-performing native experience while keeping the integration lightweight and flexible.

## Integration Modes

### SDK Prefetch Mode

The SDK automatically preloads and caches offers using a 0×0 WebView placed in advance of checkout. Offers are displayed instantly when the user reaches the checkout step.

- Easiest integration path
- No need to make manual API calls

### API Prefetch Mode

Your app uses a native HTTPS request to fetch offers from the Moments API and passes the response into the SDK during checkout.

- Full control over when and how offers are requested
- Ability to customize the request payload (e.g., user ID, cart value)

**In both modes,&#x20;**&#x74;he offers are rendered inside a WebView on the checkout screen. Offers are preloaded ahead of time, so user experience remains responsive, even when no offers are available. If no offers are returned, your app can skip rendering the offer section.

:::hint{type="success"}
To see a full implementation, check out the [MomentScience Flutter Demo on GitHub ](https://github.com/AdsPostX/examples/tree/main/flutter/MSSDKDemoApp-Flutter)which includes examples for offer fetching, event handling, and WebView integration.&#x20;
:::

***

## Requirements

To integrate the MomentScience SDK into your Flutter app, ensure the following prerequisites are met:

- A vali&#x64;**&#x20;MomentScience SDK ID**, which you can [obtain by following these steps.](https://docs.momentscience.com/getting-your-sdk-id)
- **Environment**:
  - **Flutter:** `3.0.0+`
  - **Dart:** `2.17.0+`
  - **iOS minimum deployment target:&#x20;**&#x69;OS `12`
  - **Android minimum SDK version:** API level `21`

***

# Integration Steps

## Step 1:  Add Dependencies

### Add HTML Assets

Add the required HTML templates to your project. These files are used by the SDK to render the offer experience in a WebView.

1. Create a directory: `assets/html/`
2. Place the following files in the `assets/html/` directory:
   - [`checkout.html`](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/assets/html/checkout.html)
   - [`prefetch.html`](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/assets/html/prefetch.html)
3. Register the asset path in your `pubspec.yaml`:

:::CodeblockTabs{indent="2"}
pubspec.yaml

```yaml
flutter:
  assets:
    - assets/html/
```
:::

### Install Dependencies

Update your `pubspec.yaml` file with the required packages:

```yaml
dependencies:
  http: ^1.1.0               # For making API requests
  device_info_plus: ^9.1.0   # For retrieving user-agent information
  flutter_inappwebview: ^6.1.5 # For rendering the offers in WebView
  url_launcher: ^6.1.10      # For launching URLs externally
  flutter_dotenv: ^5.0.2     # For managing environment variables (optional)
```

After adding the dependencies, install them by running the following command from your project root:

```shell
flutter pub get
```

### Add Internet Permission

To allow offer content to load, add the following permission in `android/app/src/main/AndroidManifest.xml`inside `<manifest>` but before `<application>`:&#x20;

```xml
<uses-permission android:name="android.permission.INTERNET" />
```

This is required for the SDK to load offer content in both integration modes.

***

## Step 2: Prefetch Offers (SDK or API)

You can prefetch offers using one of two modes:

- **SDK Prefetch**: Uses a hidden 0×0 WebView to preload offers.
- **API Prefetch**: Uses a native API call to fetch offers, which are then injected into the SDK display template.

In both modes, you can skip showing the checkout screen if no offers are found.

### Option 1: SDK Prefetch

This method uses a hidden 0×0 WebView to preload the Moments SDK in the background. When available offers are detected, the SDK triggers an `ads_found` event in your app.
This is the most lightweight integration method and is ideal when you want to minimize native code involvement.

1. **Set up the WebView:** Place a hidden 0×0 WebView on any screen before checkout.
   Make sure to enable the following WebView settings:
   - JavaScript
   - DOM storage
   - Database support
2. **Register JavaScript Event Handler:&#x20;**&#x54;he Web SDK communicates with your app through JavaScript events. You must handle these events in your app to react to SDK events. Use `addJavaScriptHandler` to listen for Web SDK events, and implement a handler like:

:::CodeblockTabs{indent="2"}
addJavaScriptHandler

```dart
InAppWebView(
  onWebViewCreated: (controller) {
    _webViewController = controller;

    controller.addJavaScriptHandler(
      handlerName: 'adpxCallback',
      callback: (args) {
        if (args.length >= 2) {
          String event = args[0];
          String payload = args[1];
          _prefetchService.handleWebSDKEvent(event, payload);
        }
        return null;
      },
    );
  },
)
```

handleWebSDKEvent

```dart
void handleWebSDKEvent(String event, String payload) {
  debugPrint('WebSDK event received: $event');

  if (event == 'ads_found') {
    int offerCount = getOfferCountFromWebSDKPayload(payload);
    debugPrint('Offers found: $offerCount');

    // Notify the app/UI with the number of offers found
    _webSDKCallback?.call(offerCount);
  }
}

```

Helper to extract the offer count

```dart
int getOfferCountFromWebSDKPayload(String payload) {
  try {
    final payloadObj = jsonDecode(payload);

    final response = payloadObj['response'];
    final data = response['data'];
    final offers = data['offers'];

    if (offers is List) return offers.length;
  } catch (e) {
    debugPrint('Error parsing WebSDK payload: $e');
  }
  return 0;
}
```
:::

3. **Load WebView with the Prefetch HTML Template:&#x20;**&#x55;se the existing [`prefetch.html`](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/assets/html/prefetch.html) template from your local assets. Inject your SDK ID, SDK CDN URL, and AdpxUser configuration (payload) dynamically, then load the resulting HTML into the WebView.****


:::CodeblockTabs{indent="2"}
prefetchWithWebSDK

```dart
// prefetchWithWebSDK() replaces placeholders in prefetch.html
// with actual values, such as sdkId, payload, and config.
// It then loads the final HTML into the webview,
// which initializes the SDK in prefetch mode
Future<void> prefetchWithWebSDK(String sdkId, InAppWebViewController? webViewController) async {
  if (webViewController == null) throw Exception("WebView controller is not initialized");

  final payload = await createPayload();
  final htmlTemplate = await rootBundle.loadString('assets/html/prefetch.html');

  // Insert user config as window.AdpxUser
  String adpxUserConfigScript = '';
  if (payload.isNotEmpty) {
    adpxUserConfigScript = 'window.AdpxUser = {\n';
    payload.forEach((key, value) {
      adpxUserConfigScript += '  $key: "$value",\n';
    });
    adpxUserConfigScript = adpxUserConfigScript.replaceFirst(RegExp(r',\n$'), '\n') + '};\n';
  }

  // Inject into template
  String finalHtml = htmlTemplate
    .replaceAll('%%SDK_ID%%', sdkId)
    .replaceAll('%%SDK_CDN_URL%%', AppConfig.web.launcherScriptURL)
    .replaceFirst('window.AdpxUser = {};', adpxUserConfigScript);

  await webViewController.loadData(
    data: finalHtml,
    mimeType: 'text/html',
    encoding: 'utf-8',
    baseUrl: WebUri('https://myapp.local'), // Avoid using about:blank to prevent WebView security errors
  );

  debugPrint('WebSDK prefetch initialized with SDK ID: $sdkId');
}
```

createPayload

```dart
  Future<Map<String, String>> createPayload() async {
    return {
      'pub_user_id': <"unique_value">, // A unique, non-PII identifier for the user
      'adpx_fp': <"unique_value">, // Unique user identifier
      'ua': await DeviceUtils.getUserAgent(), //User agent string
      'themeId': 'demo', // pass valid themeId
      'placement': 'checkout', //Indicates where the Offer Unit is shown (e.g., checkout_screen)
    };
  }
```
:::

4. **Handle&#x20;**`ads_found`**&#x20;Event:&#x20;**&#x4F;nce the WebView receives the `ads_found` event, extract the offer count. If `offerCount == 0`, you can skip rendering the offer UI entirely.

:::hint{type="info"}
See [prefetch\_service.dart](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/lib/services/prefetch_service.dart) in the demo app for working implementation.
:::

### Option 2: Prefetch with API

This method allows your app to prefetch offers directly from the Moments API and pass them into the Web SDK via script injection.

:::hint{type="success"}
For complete details on MomentPerks API, refer to the [MomentPerks API.](docId\:zpOL4DX0Bohl2AOoyZrLQ) documentation.
:::

1. **Send a&#x20;**`POST`**request to the Moments API:**

:::CodeblockTabs{indent="2"}
Moments API Endpoint

```javascript
POST https://api.adspostx.com/native/v4/offers.json
```
:::

2. **Build the Request Parameters:&#x20;**&#x49;nclude the following:
   1. `api_key`as a query parameter
   2. A user payload in the request body
   3. Custom `User-Agent` in the request headers
3. **Validate Optional Parameters (if used):&#x20;**
   1. `loyaltyboost `must be "`0`", "`1`", or "`2`"
   2. `creative `must be "`0`" or "`1`"
4. **Prepare and Send the Request:&#x20;**&#x55;se the following example code to construct and send the request:

:::CodeblockTabs{indent="2"}
fetchOffers Function

```dart
Future<Map<String, dynamic>> fetchOffers({
  required String sdkId,
  bool isDevelopment = false,
  Map<String, String>? payload,
  String? loyaltyboost,
  String? creative,
  String? campaignId,
}) async {
  if (loyaltyboost != null && !['0', '1', '2'].contains(loyaltyboost)) {
    throw NetworkException(NetworkError.invalidParameter, 'loyaltyboost must be 0, 1, or 2');
  }

  if (creative != null && !['0', '1'].contains(creative)) {
    throw NetworkException(NetworkError.invalidParameter, 'creative must be 0 or 1');
  }

  try {
    final uri = Uri.parse(AppConfig.api.baseURL);
    final queryParams = {
      'api_key': sdkId,
      if (loyaltyboost != null) 'loyaltyboost': loyaltyboost,
      if (creative != null) 'creative': creative,
      if (campaignId != null) 'campaignId': campaignId,
    };

    final url = Uri(
      scheme: uri.scheme,
      host: uri.host,
      path: uri.path,
      queryParameters: queryParams,
    );

    final requestBody = <String, dynamic>{};
    if (isDevelopment) requestBody['dev'] = '1';
    if (payload != null) requestBody.addAll(payload);

    final String userAgent = payload?['ua'] ?? await DeviceUtils.getUserAgent();
    final jsonBody = jsonEncode(requestBody);

    final response = await _client.post(
      url,
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': userAgent,
      },
      body: jsonBody,
    );

    if (response.statusCode >= 200 && response.statusCode < 300) {
      return jsonDecode(response.body) as Map<String, dynamic>;
    } else {
      throw NetworkException(
        NetworkError.serverError,
        'Server returned error: ${response.statusCode}',
      );
    }
  } catch (e) {
    debugPrint('Network error: $e');
    throw NetworkException(
      NetworkError.connectionError,
      'Failed to connect: ${e.toString()}',
    );
  }
}
```

createPayload

```dart
Future<Map<String, String>> createPayload() async {
  return {
    'pub_user_id': "<unique_value>", // Simple unique ID
    'adpx_fp': "<unique_value>",     // Unique value
    'ua': await DeviceUtils.getUserAgent(), //Always set a valid User-Agent. 
    'themeId': 'demo', // pass valid themeId here
    'placement': 'checkout',
  };
```
:::

5. **Save the API Response Locally:&#x20;**&#x53;tore the JSON response from the API to use it when initializing the SDK

:::hint{type="info"}
See [prefetch\_service.dart](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/lib/services/prefetch_service.dart), [network\_service.dart](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/lib/services/network_service.dart) and [device\_utils.dart](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/lib/utils/device_utils.dart) in the demo app for a complete working implementation of this flow.
:::

***

## Step 3: Show Offers Using Fullscreen WebView

After confirming that offers are available, display them on your checkout screen using a fullscreen WebView. Pass the necessary parameters depending on the prefetch method.

1. **Navigate to the Checkout Screen:&#x20;**&#x55;se `Navigator.push()` to launch a `CheckoutScreen`and pass the required values

:::CodeblockTabs{indent="2"}
```dart
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => CheckoutScreen(
      sdkId: _sdkIdController.text.trim(),
      prefetchMethod: prefetchMethod, // you may not need this parameter as you are going to use only one of the approach.
      apiResponse: apiResponse, // Only required for API prefetch
      offerCount: _offerCount, // Optional, require only if you want to show no of offers in UI.
      payload: payload,
    ),
  ),
);
```
:::

2. **Load the Offer Experience:&#x20;**&#x49;nside the `CheckoutScreen`, load the offer experience into the WebView using [`checkout.html`](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/assets/html/checkout.html). Follow these steps:
   1. Read [`checkout.html`](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/assets/html/checkout.html) from assets.
   2. Replace placeholders like `%%SDK_ID%%`, `%%CONFIG%%`, and` %%AdpxUser%%` with actual values (from the payload or API response).
   3. Load the final HTML into the WebView.
   4. Ensure JavaScript, DOM storage, and database settings are enabled in the WebView.
   5. Use `_getAutoLoadConfig()` (See example below) to determine whether auto-loading and prefetc should be enabled based on your prefetch method:
3. **Choose Logic Based on Prefetch Mode:&#x20;**&#x55;se different logic depending on whether you're using:
   - **Prefetch with SDK:** Cached offers are automatically shown by the SDK.
   - **Prefetch with API:** You need to pass the saved apiResponse and inject it via JavaScript.

:::CodeblockTabs
loadContentIntoWebView

```dart
Future<void> loadContentIntoWebView({
  required InAppWebViewController controller,
  required String sdkId,
  required bool isPrefetchApi,
  Map<String, dynamic>? apiResponse,
  required int offerCount,
  Map<String, String>? payload,
}) async {
  try {
    // Reset the externally opened URLs when loading new content
    resetExternallyOpenedUrls();

    // Get SDK CDN URL from AppConfig
    final String sdkCdnUrl = AppConfig.web.launcherScriptURL;

    // Load HTML template from assets
    final String htmlTemplate = await rootBundle.loadString('assets/html/checkout.html');

    // Create AdpxUser script with payload key-value pairs
    String adpxUserScript = _createAdpxUserScript(payload);

    // Different config based on prefetch method
    String autoLoadConfig = _getAutoLoadConfig(isPrefetchApi);

    // Prepare response handling code - only for API prefetch method
    String responseHandling = _createResponseHandlingScript(isPrefetchApi, apiResponse);

    // Replace placeholders with actual values
    String html = htmlTemplate
        .replaceAll('%%SDK_ID%%', sdkId)
        .replaceAll('%%SDK_CDN_URL%%', sdkCdnUrl)
        .replaceAll('%%OFFERS_COUNT%%', offerCount.toString())
        .replaceAll('%%AUTOLOAD_CONFIG%%', autoLoadConfig)
        .replaceAll('%%RESPONSE_HANDLING%%', responseHandling)
        .replaceAll('window.AdpxUser = {};', adpxUserScript);

    // Load HTML content directly with a proper baseUrl
    await controller.loadData(
      data: html,
      mimeType: 'text/html',
      encoding: 'utf-8',
      baseUrl: WebUri(
        'https://myapp.local',
      ), // Required to avoid security errors; 'about:blank' or skipping this causes issues.
    );

    debugPrint('WebView initialized with SDK ID: $sdkId');
  } catch (e) {
    debugPrint('Error loading HTML content: $e');
    rethrow;
  }
}
```

\_createAdpxUserScript

```dart
String _createAdpxUserScript(Map<String, String>? payload) {
  String adpxUserScript = 'window.AdpxUser = {';

  if (payload != null && payload.isNotEmpty) {
    payload.forEach((key, value) {
      adpxUserScript += '\n  $key: "$value",';
    });

    // Remove trailing comma
    adpxUserScript = adpxUserScript.substring(0, adpxUserScript.length - 1);
  }

  adpxUserScript += '\n};';
  return adpxUserScript;
}

```

\_getAutoLoadConfig

```dart
String _getAutoLoadConfig(bool isPrefetchApi) {
  if (isPrefetchApi) {
    // For API prefetch: Disable auto features since we'll provide the response
    return 'autoLoad: false, prefetch: false';
  } else {
    // For WebSDK prefetch: Enable auto features to use cached data
    return 'autoLoad: true, prefetch: true';
  }
}

```

\_createResponseHandlingScript

```dart
String _createResponseHandlingScript(
  bool isPrefetchApi,
  Map<String, dynamic>? apiResponse,
) {
  if (isPrefetchApi && apiResponse != null) {
    // Prepare the JSON string for the API response
    final apiResponseJson = jsonEncode(apiResponse);

    return '''
// Add the API response to Adpx
if (window.Adpx && window.Adpx.setApiResponse) {
  const apiResponse = $apiResponseJson;
  window.Adpx.setApiResponse(apiResponse).then(() => {
    console.log('API response set to Adpx, now reloading...');
    window.Adpx.reload();
  });
}
''';
  }

  return '';
}

```
:::

:::hint{type="info"}
See [checkout\_service.dart](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/lib/services/checkout_service.dart) and [checkout\_screen.dart](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/lib/screens/checkout_screen.dart) in the demo app for working implementation.
:::

## Step 4: Open Clicks in an External Browser

To ensure that offer clickouts open in the device’s default browser (rather than inside the WebView), listen for SDK callback events and handle the URL externally.

1. **Add a JavaScript Handler in the WebView:** Use `addJavaScriptHandler`to capture SDK events such as `url_clicked`

:::CodeblockTabs{indent="2"}
addJavaScriptHandler

```dart
controller.addJavaScriptHandler(
  handlerName: 'adpxCallback',
  callback: (args) {
    if (args.length >= 2) {
      String event = args[0];
      dynamic payload = args[1];
      debugPrint('WebView callback: $event, $payload');

      _checkoutService.handleAdEvent(event, payload);
    }
    return null;
  },
);
```

processUrlEvent

```dart
  Future<void> handleAdEvent(String event, dynamic payload) async {
    debugPrint('Processing ad event: $event');

    // Handle ad_taken event that requires opening an external URL
    if (event == 'url_clicked') {
      await _processUrlEvent(payload);
    }
  }

Future<void> _processUrlEvent(dynamic payload) async {
  try {
    // Parse payload if it's a string
    Map<String, dynamic> payloadMap;

    if (payload is String) {
      try {
        payloadMap = Map<String, dynamic>.from(json.decode(payload));
      } catch (e) {
        debugPrint('Error parsing payload: $e');
        return;
      }
    } else if (payload is Map) {
      payloadMap = Map<String, dynamic>.from(payload);
    } else {
      debugPrint('Unexpected payload type: ${payload.runtimeType}');
      return;
    }

    // Extract target_url and open in external browser
    if (payloadMap.containsKey('target_url')) {
      final targetUrl = payloadMap['target_url'];
      if (targetUrl != null && targetUrl is String) {
        final normalizedUrl = _normalizeUrl(targetUrl);
        _externallyOpenedUrls.add(normalizedUrl);
        await _openExternalUrl(normalizedUrl);
      }
    }
  } catch (e) {
    debugPrint('Error processing URL event: $e');
  }
}

```
:::

2. **Handle the&#x20;**`url_clicked`**Event in Your Service:&#x20;**&#x4F;pen external URLs manually and prevent the WebView from navigating to the same URL again.

:::CodeblockTabs{indent="2"}
handleUrlNavigation

```dart
Future<NavigationActionPolicy> handleUrlNavigation(NavigationAction navigationAction) async {
  final uri = navigationAction.request.url;
  if (uri != null) {
    await Future.delayed(const Duration(seconds: 1));

    final url = uri.toString();
    final normalizedUrl = _normalizeUrl(url);
    debugPrint('Intercepted URL: $normalizedUrl');

    if (_externallyOpenedUrls.contains(normalizedUrl)) {
      debugPrint('Canceling navigation for externally opened URL: $normalizedUrl');
      return NavigationActionPolicy.CANCEL;
    }

    return NavigationActionPolicy.ALLOW;
  }

  return NavigationActionPolicy.CANCEL;
}
```
:::

:::hint{type="info"}
See [checkout\_screen.dart ](https://github.com/AdsPostX/examples/blob/main/flutter/MSSDKDemoApp-Flutter/mssdk_demo_app/lib/screens/checkout_screen.dart)in the demo app for working implementation.
:::

# Conclusion

Congratulations! You've now completed the integration of the Moments solutioninto your Flutter app.

Whether you're using **SDK Prefetch Mode** or **API Prefetch Mode**, your app is now ready to deliver personalized offers directly within the user journey, enhancing monetization while maintaining full control over offer timing and display.

***

📢 If you're running into any issues while going through the integration process, feel free to contact us at [help@momentscience.com](mailto\:help@Momentscience.com)
