pnlight_sdk 0.10.0 copy "pnlight_sdk: ^0.10.0" to clipboard
pnlight_sdk: ^0.10.0 copied to clipboard

PlatformiOS

Flutter wrapper for PNLight iOS SDK.

PNLight SDK - Flutter Plugin #

pub package

A Flutter plugin that provides iOS integration with the PNLight SDK.

Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  pnlight_sdk: ^0.1.0

Then run:

flutter pub get

Requirements #

  • Flutter: >=3.3.0
  • Dart: >=2.17.0 <4.0.0
  • iOS: 15.0+

This package is iOS-only. Android is not supported.


iOS Setup #

pnlight_sdk declares DivKit, the DivKit extension runtime, Lottie, and Core Haptics transitively. After adding the Flutter package, no separate pod, renderer registration, navigation controller, or safe-area integration is required for RemoteUiView.

The pod also links PNLight's required Apple frameworks automatically. Apps that need IDFA tracking must still provide the user-facing NSUserTrackingUsageDescription text in Info.plist:

<key>NSUserTrackingUsageDescription</key>
<string>This app uses device tracking to provide analytics and improve user experience.</string>

Usage #

Initialization #

Initialize PNLight before using analytics, attribution, Remote UI, or Remote Config. You can provide local Remote Config defaults at startup: they work offline, are never sent to PNLight, and are used if a remote value is missing or has a different type.

import 'package:flutter/widgets.dart';
import 'package:pnlight_sdk/pnlight_sdk.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await PNLightSDK.initialize(
    'your-api-key',
    remoteConfigDefaults: {
      'paywall_enabled': true,
      'paywall_title': 'Try Premium',
      'trial_days': 7,
      'enabled_plans': ['monthly', 'yearly'],
      'paywall_style': {'accent': '#6750A4', 'compact': false},
    },
  );

  runApp(MyApp());
}

Event Logging #

import 'package:pnlight_sdk/pnlight_sdk.dart';

await PNLightSDK.logEvent('purchase_completed', {
  'product_id': 'premium_subscription',
  'amount': 9.99,
  'currency': 'USD',
});

Attribution #

Send attribution data from external providers before requesting UI config.

import 'package:pnlight_sdk/pnlight_sdk.dart';

final success = await PNLightSDK.addAttribution(
  provider: 'appsFlyer',
  data: {'af_status': 'Non-organic'},
  identifier: 'your-appsflyer-id',
);

AppsFlyer Integration Example

PNLight does not depend on the AppsFlyer initialization order — it only needs the conversion data, delivered via addAttribution. Make sure the conversion ("attribution success") callback is not processed before PNLight is initialized: if it can fire earlier, store the conversion data in memory and call addAttribution once initialize completes.

AppsFlyer requires the ATT prompt to complete before it starts. If PNLight is initialized before ATT authorization, call updateIdfa() after the prompt completes so PNLight receives the granted IDFA.

import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:appsflyer_sdk/appsflyer_sdk.dart';
import 'package:pnlight_sdk/pnlight_sdk.dart';

Future<void> startSdks() async {
  await PNLightSDK.initialize('your-api-key');

  // Request ATT, then pass the granted IDFA to PNLight.
  await AppTrackingTransparency.requestTrackingAuthorization();
  await PNLightSDK.updateIdfa();

  // Start AppsFlyer after ATT completes (an AppsFlyer requirement).
  final appsFlyerSdk = AppsflyerSdk(AppsFlyerOptions(
    afDevKey: 'your-appsflyer-dev-key',
    appId: 'your-ios-app-id',
    showDebug: false,
  ));

  appsFlyerSdk.onInstallConversionData((conversionData) async {
    final appsFlyerId = await appsFlyerSdk.getAppsFlyerUID();
    final payload = conversionData['payload'] ?? conversionData;

    await PNLightSDK.addAttribution(
      provider: 'appsFlyer',
      data: Map<String, dynamic>.from(payload as Map),
      identifier: appsFlyerId,
    );
  });

  await appsFlyerSdk.initSdk(
    registerConversionDataCallback: true,
    registerOnAppOpenAttributionCallback: false,
    registerOnDeepLinkingCallback: false,
  );
}

Supported providers:

  • appsFlyer
  • firebase
  • facebook

User Identity #

import 'package:pnlight_sdk/pnlight_sdk.dart';

final userId = await PNLightSDK.getUserId();

In-App Purchases #

PNLight wraps StoreKit 2 for fetching products (price, offers, trial info), purchasing, restoring, and checking entitlements. The product ids are configured on the backend — fetchProducts resolves them against the App Store.

import 'package:pnlight_sdk/pnlight_sdk.dart';

// Load the configured products with their App Store price/offer info.
final products = await PNLightSDK.fetchProducts();
for (final product in products) {
  print('${product.displayName}: ${product.displayPrice}');

  final offer = product.subscription?.introductoryOffer;
  if (offer != null && product.subscription!.isEligibleForIntroOffer) {
    // e.g. pay-as-you-go: "$0.99/month for 6 months"
    print('Offer: ${offer.displayPrice} (${offer.paymentMode.name}, '
        '${offer.periodCount} × ${offer.period.value} ${offer.period.unit.name})');
  }
}

// Purchase.
final result = await PNLightSDK.purchase('your.product.id');
if (result == PNLightPurchaseResult.success) {
  // Unlock content.
}

// Entitlement checks (local StoreKit entitlements, work offline).
final premium = await PNLightSDK.isPremium();
final eligible = await PNLightSDK.isEligibleForTrial('your.product.id');

// Restore previous purchases.
await PNLightSDK.restorePurchases();

RemoteUiView - Server-driven UI #

RemoteUiView fetches and renders a server-driven layout from PNLight for a given placement. It calls getUIConfig(placement) internally, renders the native view, and emits action events to Dart.

DivKit markup can also select PNLight's three custom native iOS components: pnlight.circular_loader, pnlight.cta_button, and pnlight.icon_button. They are UIKit views rendered inside the iOS platform view; Dart does not recreate them. Their layout, styling, state, and actions are still controlled by the server-delivered DivKit JSON. These custom components are unavailable on non-iOS platforms.

When using external attribution providers such as AppsFlyer, send attribution as early as possible (see the AppsFlyer example above). getUIConfig waits for attribution data internally when attributionRequired is true (the default), so no manual delay is needed.

import 'package:flutter/widgets.dart';
import 'package:pnlight_sdk/pnlight_sdk.dart';

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

  void _handleAction(RemoteUiActionEvent event) {
    if (event.logId == 'purchase_button') {
      final productId = event.params['id'];
      // Start purchase flow for productId
    } else if (event.logId == 'close_button') {
      // Close the screen
    }
  }

  @override
  Widget build(BuildContext context) {
    return RemoteUiView(
      placement: 'paywall',
      onAction: _handleAction,
    );
  }
}

Remote UI schema version #

PNLight versions the Remote UI envelope independently from the pub package. Declare "schemaVersion": 2 at the document root to enable PNLight safe-area handling, linear-scaling variables, server-driven flows, native haptics, and native dialogs:

{
  "schemaVersion": 2,
  "card": {}
}

A missing schemaVersion (or an explicit value of 1) is legacy v1. Existing backend configs therefore keep their previous edge-to-edge DivKit behavior when an app upgrades the plugin. They do not receive v2 scaling variables or opt into PNLight-owned navigation, haptic, and dialog action handling. New configs that use any feature below should declare "schemaVersion": 2.

Safe areas #

Remote UI stays clear of the Dynamic Island, status bar, home indicator, and landscape sensor housing without Flutter-side padding in schema v2. A v2 document with no safe_area field defaults to inset on all four edges:

{
  "schemaVersion": 2,
  "safe_area": {
    "mode": "inset",
    "edges": ["top", "bottom", "left", "right"]
  },
  "card": {}
}
Mode Behavior
inset Default. Places the complete DivKit document inside the selected safe-area edges.
content Keeps the document edge-to-edge and exposes selected insets as DivKit variables.
edge_to_edge Keeps the document edge-to-edge and resolves safe-area variables to zero.

In content mode, use safe_area_top, safe_area_bottom, safe_area_left, and safe_area_right in the content container while its background remains full bleed:

{
  "schemaVersion": 2,
  "safe_area": {
    "mode": "content",
    "edges": ["top", "bottom"]
  },
  "card": {
    "states": [{
      "state_id": 0,
      "div": {
        "type": "container",
        "width": { "type": "match_parent" },
        "height": { "type": "match_parent" },
        "paddings": {
          "top": "@{safe_area_top + 24}",
          "bottom": "@{safe_area_bottom + 24}",
          "left": 24,
          "right": 24
        },
        "items": []
      }
    }]
  }
}

PNLight updates these variables after rotation, window resizing, and sheet presentation. A flow can declare a default safe_area; each route can override it beside divkit and presentation.

Linear scaling variables #

Remote UI JSON may declare a logical design viewport:

{
  "schemaVersion": 2,
  "referenceSize": { "width": 390, "height": 844 },
  "card": {}
}

PNLight exposes scaleX = availableWidth / referenceSize.width and scaleY = availableHeight / referenceSize.height as live numeric DivKit variables. Use them directly in expressions, for example "left": "@{24 * scaleX}". They update for rotation, host-view resize, and native sheet-size changes. In inset safe-area mode, the available viewport is measured after the selected system insets are removed.

The ratios are raw, not automatically clamped, and PNLight does not scale the layout unless markup references them. For a uniform mobile design scale, prefer scaleX and apply your chosen bounds in markup. Never multiply system safe-area or keyboard insets by these values. Without referenceSize, both variables equal 1 in schema v2.

Flows may define referenceSize beside routes; individual routes may override it beside divkit or inside their complete divkit document. No Flutter configuration is required.

Server-driven native flows #

The same RemoteUiView also accepts a PNLight flow envelope. Each route contains a complete DivKit document. PNLight owns a private native navigation stack and consumes navigation URLs before they reach Dart:

{
  "schemaVersion": 2,
  "type": "flow",
  "safe_area": { "mode": "inset" },
  "initial_route": "welcome",
  "routes": {
    "welcome": {
      "divkit": {
        "templates": {},
        "card": {
          "log_id": "welcome",
          "states": [{
            "state_id": 0,
            "div": {
              "type": "custom",
              "custom_type": "pnlight.cta_button",
              "width": { "type": "match_parent" },
              "height": { "type": "fixed", "value": 56 },
              "custom_props": {
                "title": "Open offer",
                "url": "pnlight://navigation/present?route=offer"
              }
            }
          }]
        }
      }
    },
    "offer": {
      "safe_area": {
        "mode": "content",
        "edges": ["top", "bottom"]
      },
      "presentation": {
        "style": "sheet",
        "detent": "large",
        "grabber": true,
        "dismissible": true,
        "corner_radius": 28
      },
      "divkit": {
        "templates": {},
        "card": {
          "log_id": "offer",
          "states": [{
            "state_id": 0,
            "div": {
              "type": "custom",
              "custom_type": "pnlight.cta_button",
              "width": { "type": "match_parent" },
              "height": { "type": "fixed", "value": 56 },
              "custom_props": {
                "title": "Dismiss",
                "url": "pnlight://navigation/dismiss"
              }
            }
          }]
        }
      }
    }
  }
}
URL Native iOS behavior
pnlight://navigation/push?route=details Pushes a route on the active PNLight stack.
pnlight://navigation/pop Pops the active stack.
pnlight://navigation/replace?route=details Replaces the active route.
pnlight://navigation/pop_to_root Pops to the first route.
pnlight://navigation/present?route=offer Presents a route in a new native modal navigation context.
pnlight://navigation/dismiss Dismisses the current PNLight modal context.

Routes are preloaded when the flow is received. A presented route can push other routes inside its own stack, while dismissing it preserves the embedded stack's controllers, DivKit variables, and scroll state.

presentation.style accepts sheet (default) or full_screen. A sheet uses the native iOS page-sheet presentation:

Field Default Description
detent large large, or medium with expansion to large.
grabber true Shows the native sheet grabber.
dismissible true When false, disables swipe-to-dismiss.
corner_radius system Preferred native sheet corner radius.

The present URL can override those fields for one transition, for example pnlight://navigation/present?route=offer&detent=medium&grabber=false. Unknown routes are ignored. In a legacy single-card document, navigation URLs continue through onAction; they are consumed only inside a valid flow.

Native alerts and action sheets #

Put dialogs at the Remote UI document root (beside card, or beside routes in a flow) and trigger one with pnlight://dialog/show?id=<dialog-id>. The plugin presents a native iOS UIAlertController; Flutter needs no navigator, dialog state, or additional configuration.

{
  "schemaVersion": 2,
  "dialogs": {
    "delete_confirmation": {
      "style": "alert",
      "title": "Delete item?",
      "message": "This cannot be undone.",
      "buttons": [
        { "title": "Cancel", "style": "cancel", "actions": [] },
        {
          "title": "Delete",
          "style": "destructive",
          "actions": [
            {
              "log_id": "delete_confirmed",
              "url": "my-app://delete?id=42"
            }
          ]
        }
      ]
    }
  },
  "card": {}
}

Dialog style is alert or action_sheet; button style is default, cancel, or destructive. Every button owns an ordered array of normal DivKit actions. They run through the current card's DivKit handler, including typed variable/state actions, PNLight navigation and haptics, analytics, and custom URLs delivered through onAction. Empty arrays are dismiss-only.

Native haptics #

PNLight consumes pnlight://haptic/... actions before onAction. Simple UIKit feedback works in both schema-v2 single-card documents and flow routes:

URL Native behavior
pnlight://haptic/impact?style=light Impact feedback. style: light, medium, heavy, soft, or rigid; optional intensity is clamped to 0...1.
pnlight://haptic/selection Selection feedback.
pnlight://haptic/notification?type=success Notification feedback. type: success, warning, or error.

Flows can also declare named Core Haptics patterns:

{
  "schemaVersion": 2,
  "type": "flow",
  "initial_route": "main",
  "haptics": {
    "ambient_pulse": {
      "events": [
        {
          "type": "continuous",
          "time": 0,
          "duration": 0.8,
          "intensity": 0.25,
          "sharpness": 0.35
        },
        {
          "type": "transient",
          "time": 0.4,
          "intensity": 0.65,
          "sharpness": 0.55
        }
      ],
      "loop": true,
      "max_duration": 120
    }
  },
  "routes": {
    "main": {
      "divkit": {
        "templates": {},
        "card": {
          "log_id": "main",
          "states": [{
            "state_id": 0,
            "div": {
              "type": "text",
              "text": "Haptic demo",
              "width": { "type": "match_parent" },
              "height": { "type": "wrap_content" }
            }
          }]
        }
      }
    }
  }
}

Use pnlight://haptic/start?pattern=ambient_pulse to start a named pattern and pnlight://haptic/stop?pattern=ambient_pulse to stop it. A stop URL without pattern stops every active player owned by the route.

Patterns accept 1–128 transient or continuous events. Timing is in seconds; time_ms, duration_ms, and max_duration_ms are also accepted. Intensity and sharpness are clamped to 0...1. A pattern can run for at most 600 seconds and defaults to a five-minute safety limit. Core Haptics is a no-op on unsupported hardware. Active players stop when their route leaves the window, the configuration changes, the app enters the background, or the renderer is destroyed.

Lottie animations #

pnlight_sdk registers DivKit's standard lottie extension automatically. The Flutter app does not install Lottie or register a renderer.

Attach the extension to a fixed-size DivKit element and provide a remote JSON URL:

{
  "type": "container",
  "width": { "type": "fixed", "value": 200 },
  "height": { "type": "fixed", "value": 200 },
  "items": [],
  "extensions": [{
    "id": "lottie",
    "params": {
      "lottie_url": "https://cdn.example.com/animation.json",
      "repeat_count": 0,
      "repeat_mode": "restart",
      "is_playing": true
    }
  }]
}

Alternatively, put the decoded Lottie document directly in lottie_json:

{
  "id": "lottie",
  "params": {
    "lottie_json": {
      "v": "5.7.4",
      "fr": 60,
      "ip": 0,
      "op": 120,
      "w": 200,
      "h": 200,
      "assets": [],
      "layers": []
    }
  }
}

repeat_mode accepts restart (default) or reverse. A repeat_count of 0 repeats indefinitely, and is_playing defaults to true. Remote URLs use the DivKit resource pipeline; use HTTPS in production. The integration supports inline and downloaded uncompressed Lottie JSON, but not .lottie ZIP archives.

Native iOS circular loader #

Use DivKit's custom element to render a native UIActivityIndicatorView inside Remote UI markup:

{
  "type": "custom",
  "custom_type": "pnlight.circular_loader",
  "width": { "type": "fixed", "value": 48 },
  "height": { "type": "fixed", "value": 48 },
  "custom_props": {
    "style": "large",
    "color": "#FF007AFF",
    "accessibility_label": "Loading"
  }
}

style accepts "medium" (the default) or "large". color accepts #RRGGBB or DivKit-style #AARRGGBB and defaults to the adaptive iOS label color. accessibility_label defaults to "Loading". Standard DivKit width and height fields control the element's layout; set a dimension to { "type": "wrap_content" } to use the native indicator's intrinsic size for that dimension.

Native iOS CTA button #

Render a native, animated call-to-action button with a repeating shimmer streak, an idle attention pulse, and a spring press bounce. Taps use the same action pipeline as DivKit buttons, so a custom-scheme url fires RemoteUiView.onAction with a RemoteUiActionEvent.

{
  "type": "custom",
  "custom_type": "pnlight.cta_button",
  "width": { "type": "match_parent" },
  "height": { "type": "fixed", "value": 58 },
  "custom_props": {
    "title": "Continue",
    "background_color": "#FF007AFF",
    "title_color": "#FFFFFFFF",
    "corner_radius": 16,
    "font_size": 19,
    "font_weight": "bold",
    "shimmer": true,
    "bounce": true,
    "url": "pnlight://cta?id=continue"
  }
}

All props are optional; only title and url are usually needed. Colors accept #RRGGBB or DivKit-style #AARRGGBB.

Prop Default Description
title "" Button label.
background_color #FF007AFF Fill color (also the gradient start).
background_color_end When set, the fill is a horizontal gradient to this color.
glass off (on for icon buttons) Native Liquid Glass fill on iOS 26+. See below.
title_color #FFFFFFFF Label color.
corner_radius 14 Corner radius in points (continuous curve).
font_size 18 Label point size.
font_weight semibold regular/medium/semibold/bold/heavy/black.
horizontal_padding / vertical_padding 24 / 16 Used to size the button when width/height is wrap_content.
icon SF Symbol name, e.g. "shield.lefthalf.filled". Renders before the title.
icon_size 20 Symbol point size.
icon_weight semibold ultralightblack.
icon_color title_color Symbol tint.
icon_spacing 8 Gap between icon and title.
loading false Swaps the title for a native spinner and ignores taps.
disabled false Dims the button and ignores taps.
disabled_alpha 0.45 Opacity used while disabled.
disabled_background_color Replaces the fill (and any gradient) while disabled.
disabled_title_color Replaces the label color while disabled.
loading_indicator_color title_color Spinner color.
loading_indicator_style medium medium or large.
url Action fired on tap (custom scheme → onAction; http(s) → opened externally).
log_id Emitted as the action's logId when there is no url.
accessibility_label title VoiceOver label (defaults to "Loading" while loading).

Both loading and disabled make the button inert: taps are ignored and the shimmer and bounce animations stop.

shimmer and bounce accept a boolean shorthand (true/false) or an object for fine control:

{
  "shimmer": {
    "enabled": true,
    "color": "#80FFFFFF",
    "duration": 1.4,
    "pause": 1.1,
    "band_width": 0.3,
    "angle": 16
  },
  "bounce": {
    "idle": { "enabled": true, "scale": 1.03, "period": 2.6 },
    "press": { "enabled": true, "scale": 0.96 }
  }
}

{ "bounce": true } enables both the idle pulse and press feedback. { "bounce": { "idle": false, "press": true } } keeps only press feedback.

Liquid Glass (iOS 26+)

Buttons can use the native Liquid Glass material. Icon buttons opt in automatically when markup does not set background_color; they fall back to secondarySystemFill on older systems. An explicit background_color disables the automatic glass default. Set glass explicitly to combine a chosen color with the material:

{
  "glass": {
    "enabled": true,
    "style": "regular",
    "tint": "#99FF375F",
    "prominent": true
  }
}

style accepts "regular" or "clear". { "glass": true } is the shorthand for opting a CTA in. prominent selects the filled primary-action treatment. When glass is enabled, title_color defaults to the adaptive label color and shimmer defaults to off unless markup overrides them. Glass falls back to the solid background_color on iOS 25 and earlier, when Reduce Transparency is enabled, and while disabled_background_color applies.

Native CTA and icon buttons also support the standard DivKit actions array. Actions execute in declaration order through DivKit's normal action handler, including typed variable/state actions, PNLight haptics, dialogs, navigation, analytics, and custom URLs. A non-empty actions array takes precedence over the custom_props.url / log_id single-action shorthand.

Driving loading and disabled at runtime

Any custom prop can contain a DivKit expression. For example, bind loading to a card variable:

{
  "card": {
    "variables": [{ "type": "boolean", "name": "is_busy", "value": false }],
    "states": [{ "state_id": 0, "div": {
      "type": "custom",
      "custom_type": "pnlight.cta_button",
      "custom_props": {
        "title": "Submit",
        "loading": "@{is_busy}",
        "url": "pnlight://cta?id=submit"
      }
    }}]
  }
}

Toggle the variable from markup with a standard set_variable action:

{
  "log_id": "begin",
  "typed": {
    "type": "set_variable",
    "variable_name": "is_busy",
    "value": { "type": "boolean", "value": true }
  }
}

The Flutter callback receives the URL query parameters:

void handleAction(RemoteUiActionEvent event) {
  if (event.params['id'] == 'continue') {
    // Continue the flow.
  }
}

RemoteUiView(
  placement: 'paywall',
  onAction: handleAction,
);

Native iOS icon button #

pnlight.icon_button uses the same native button renderer, tuned for a small circular SF Symbol control such as close, settings, or favorite:

{
  "type": "custom",
  "custom_type": "pnlight.icon_button",
  "width": { "type": "fixed", "value": 44 },
  "height": { "type": "fixed", "value": 44 },
  "custom_props": {
    "icon": "xmark",
    "accessibility_label": "Close",
    "url": "pnlight://cta?id=close"
  }
}

It accepts every pnlight.cta_button prop above, including loading, disabled, shimmer, bounce, and expression binding. Only its defaults differ:

Prop CTA default Icon default
shape corner_radius: 14 fully circular
background_color #FF007AFF secondarySystemFill (adaptive)
glass off on when no background_color is set
icon/title color white label (adaptive)
horizontal_padding / vertical_padding 24 / 16 12 / 12
shimmer on off
bounce.idle on off
bounce.press on on

Setting corner_radius opts out of the circular shape. With wrap_content on both axes, the button sizes itself from the symbol plus padding and remains square.

For an icon-only button, set accessibility_label; otherwise VoiceOver falls back to the SF Symbol name. An unknown symbol name is reported in the card's DivKit errors instead of silently rendering an empty button.

Manual Config Fetching #

Use getUIConfig if you need to fetch the placement configuration yourself. When attributionRequired is true (the default), the SDK waits for attribution data internally before returning:

import 'package:pnlight_sdk/pnlight_sdk.dart';

final config = await PNLightSDK.getUIConfig('paywall');
final configWithoutAttributionWait = await PNLightSDK.getUIConfig(
  'paywall',
  attributionRequired: false,
);

Remote Config #

Remote Config delivers typed, flat configuration values from the PNLight dashboard. It is for non-secret product configuration, not API keys, credentials, or other sensitive data. Values are persisted per SDK token and installation user, so local defaults are available immediately and the last activated configuration remains available offline.

Fetch after initializing PNLight. fetchAndActivate waits for AppsFlyer attribution by default so server-side attribution overrides can apply. Set waitAttribution: false when a base-only response is needed immediately.

final outcome = await PNLightSDK.fetchAndActivate();

switch (outcome) {
  case RemoteConfigFetchResult.activated:
  case RemoteConfigFetchResult.notModified:
    // A current server config is active (or its ETag matched).
    break;
  case RemoteConfigFetchResult.throttled:
    // A successful fetch occurred within the minimum interval.
    break;
  case RemoteConfigFetchResult.failed:
    // Previously active values and local defaults are still safe to use.
    break;
}

final paywallEnabled = await PNLightSDK.remoteConfigBoolean(
  'paywall_enabled',
  fallback: true,
);
final title = await PNLightSDK.remoteConfigString(
  'paywall_title',
  fallback: 'Try Premium',
);
final trialDays = await PNLightSDK.remoteConfigNumber('trial_days', fallback: 7);
final plans = await PNLightSDK.remoteConfigStringArray(
  'enabled_plans',
  fallback: ['monthly'],
);
final style = await PNLightSDK.remoteConfigJSONObject(
  'paywall_style',
  fallback: {'accent': '#6750A4'},
);

Use minimumFetchInterval: Duration.zero for development and tests; production uses a 15-minute minimum by default. Remote Config keys are flat. JSON-object values are supported for structured values, but an override replaces the entire object rather than deep-merging it.


API Reference #

PNLightSDK #

Method Description
initialize(apiKey, baseDomain?, remoteConfigDefaults?) Initialize the SDK and optional local Remote Config defaults
fetchAndActivate(minimumFetchInterval?, waitAttribution?) Fetch and atomically activate Remote Config; waits for attribution by default
remoteConfigBoolean/String/Number/StringArray/JSONObject(key, fallback:) Read a typed Remote Config value with a local fallback
logEvent(eventName, eventArgs?) Log a custom event with optional arguments
addAttribution(provider, data?, identifier?) Send attribution data from AppsFlyer, Firebase, or Facebook
getUserId() Get or create a stable user identifier
updateIdfa() Send the current IDFA to PNLight after the ATT prompt completes
prefetchUIConfig(placement) Prefetch a UI config into the in-memory cache
getUIConfig(placement, attributionRequired?) Fetch a UI config; waits for attribution by default
fetchProducts() Load configured products with App Store price/offer/trial info
purchase(productId) Purchase a product; returns a PNLightPurchaseResult
restorePurchases() Restore previous purchases by syncing with the App Store
isPremium() Whether the user has an active entitlement to any configured product
isPurchased(productId) Whether the user has an active entitlement to a specific product
isEligibleForTrial(productId) Whether the user is eligible for a product's introductory offer
getAppleReceipt() Base64 App Store receipt for server-side validation, or null if absent

RemoteUiView #

Parameter Type Description
placement String PNLight placement identifier
cardId String? Card identifier; defaults to pnlight_<placement>
secure bool Deprecated. Secure rendering is controlled by the backend response.
preventRecording bool Deprecated. Capture blocking is controlled by the backend.
onAction void Function(RemoteUiActionEvent)? Called when a custom action is triggered

RemoteUiActionEvent #

Property Type Description
url String Full URL string of the triggered action
scheme String URL scheme
path String URL path component
params Map<String, String> Query parameters extracted from the URL
logId String? Log ID for the triggered action
action String? Raw action value when provided by the native view

UIConfig #

Property Type Description
config String? Remote UI JSON config
parameters Map<String, dynamic>? Placement parameters returned by PNLight
debug bool? Debug flag returned by PNLight

Support #

For support and questions, visit docs.pnlight.app.

0
likes
105
points
695
downloads

Documentation

API reference

Publisher

verified publisherpnlight.app

Weekly Downloads

Flutter wrapper for PNLight iOS SDK.

Homepage

License

unknown (license)

Dependencies

flutter

More

Packages that depend on pnlight_sdk

Packages that implement pnlight_sdk