# Changelog Source: https://docs.pnlight.app/changelog/overview Latest updates, improvements, and fixes shipped to PNLight. ## Independent Meta API connections and AppsFlyer S2S event delivery Connect only the Meta APIs you need, then send selected subscription events to AppsFlyer from the same PNLight project. ## Meta integration **Connect Meta APIs independently** Connect the Marketing API for campaigns and spend. Connect the Conversions API for server-side event delivery. Use either API or both. Target URL and click tracking stay in the same account settings. [Read the docs](/dashboard/ad-networks#connect-a-facebook-account). **Inspect each delivery attempt** When delivery fails, account settings show recent Conversions API outcomes. Open an event to see the status, reason, request URL, and HTTP response for that attempt. [Read the docs](/dashboard/events#delivery-status). ## AppsFlyer integration **Send subscription milestones through S2S** Map PNLight subscription lifecycle events to the names you expect in AppsFlyer. Pause delivery without clearing the configuration, then resume it later. Mapped events are sent for users whose attribution includes an AppsFlyer user ID. [Read the docs](/dashboard/ad-networks#connect-appsflyer-s2s-events). ## PNLight adds Remote Config for typed, targeted app settings Change a feature flag, a line of copy, a limit, or a paywall variant without waiting for the next app release. Local defaults cover first launch and offline sessions. ## Dashboard **Typed configuration and overrides** Define a base configuration with Boolean, string, number, string-array, and JSON values. Add ordered overrides for campaign, device, OS version, SDK version, or country. [Read the docs](/dashboard/remote-config#add-overrides). **Drafts and publishing** Changes stay in a draft and do not affect the app until you publish them. Publish the draft after review. To return to an earlier version, restore it into a new draft. [Read the docs](/dashboard/remote-config#save-and-publish). ## SDK updates **Local values in the SDK** Swift, React Native, and Flutter apps can supply local defaults, refresh with `fetchAndActivate`, and read values through typed getters. If a fetch fails, the previous active configuration and local defaults are still available. [Read the docs](/sdk/remote-config). ## Remote UI supports multi-screen flows and SDK-owned purchases A Remote UI placement can guide users through several screens and start a StoreKit purchase. The flow stays in remote configuration, so you do not need separate app code for each screen. ## Remote UI **Multiple screens in one flow** Return multiple routes in one flow and move between them with PNLight push, replace, present, and dismiss actions. `RemoteUiView` owns the native navigation stack and modal presentation. [Read the docs](/sdk/remote-ui#server-driven-flows). **Preview and configuration checks** Start with a reusable preset, then fill project-specific strings and product IDs through runtime placeholders. Live preview, iPhone device presets, action logs, and simulated purchase outcomes catch problems before the config reaches the app. [Read the docs](/dashboard/remote-ui-placements#preview-and-validate-a-config). **Purchases through the SDK** Schema v3 adds the SDK-owned `pnlight.purchase` action and an `onPurchased` callback for refreshing entitlements or navigation afterward. It also brings animated progress bars and numbers. Existing schema v1 and v2 configs remain supported. [Read the docs](/sdk/remote-ui#remote-ui-schema-version). **Native navigation and system feedback** Remote UI accounts for safe areas and scaling variables. Flows can also use haptics, dialogs, and native modal presentation. ## Product catalog, StoreKit 2 purchases, and event diagnostics The **Products** section now includes a catalog for App Store product IDs. The iOS SDK supports StoreKit 2 purchases and restoration, while the **Events** page shows event data and delivery attempts. ## Purchases **One catalog per project** Keep each project's App Store product IDs in **Products**, with production and QA catalogs separated by project. Remote UI product placeholders can pull IDs from the catalog while PNLight builds a config. [Read the docs](/dashboard/products). **StoreKit 2 purchases and restoration** The iOS SDK loads App Store products, completes verified purchases, restores transactions, and checks introductory-offer eligibility. Purchase and restore screens can read the same resulting state. **Receipt retries after a purchase** After StoreKit verifies a purchase, the app receives success even if PNLight cannot accept the receipt immediately. The SDK retries later, and PNLight can recover the original transaction and subscription history from the saved receipt. [Read the docs](/troubleshooting/app-store-integration#purchase-succeeds-but-does-not-appear-in-pnlight). ## Dashboard **Event data and delivery attempts** The new **Events** page brings subscription, app, and system events together across all users. Filter the list, then inspect the raw payload, attribution context, and delivery attempt for each integration. [Read the docs](/dashboard/events). ## PNLight adds Remote UI targeting, App Events analytics, and SDK 0.7.2 Remote UI gained targeting by campaign, device, and SDK version. Analytics added App Events, while SDK 0.7.2 added IDFA refresh and Remote UI cache controls. ## Dashboard **App Events analytics** The **Analytics → App Events** view includes a chart of SDK events and a searchable table. Open an event from the table to inspect its payload or related customer profile. [Read the docs](/dashboard/analytics#app-events-analytics). **Remote UI targeting and protection** Remote UI placements can now target users by ad campaign, device, OS version, country, and SDK version. Placement settings also include protection modes for secure rendering and capture handling. [Read the docs](/dashboard/remote-ui-placements#targeting-conditions). **Test devices for Remote UI QA** You can add test device IDFAs or SDK user IDs from **Integration**. Test devices can receive Remote UI in test mode, including cases where production targeting or capture protection would normally block the placement. [Read the docs](/dashboard/sdk-status#test-devices). **Customer timeline debugging** Customer profiles now provide more debugging context, including app events, attribution details, test-device state, and Remote UI capture state. [Read the docs](/dashboard/customers). ## SDK updates **SDK 0.7.2** The iOS, React Native, and Flutter wrappers are synchronized at version `0.7.2`. The release improves Remote UI cache behavior, adds `ignoreCache`, and exposes clearer config-loading errors in wrapper APIs. [Read the docs](/sdk/remote-ui#config-cache-behavior). **IDFA refresh after ATT** SDKs now expose `updateIdfa()` so apps can send the granted IDFA after the App Tracking Transparency prompt completes. This is useful when PNLight initializes before ATT authorization. [Read the docs](/sdk/attribution#recommended-appsflyer-flow). **App-open tracking** The SDK now reports repeat app opens after the first install. PNLight uses this signal to keep customer activity fresh and to add system app-open events to timelines. **Remote UI cache refresh** Remote UI configs are cache-first by default and refresh in the background with ETag requests. Use `ignoreCache` when QA or state changes require a fresh server response. [Read the docs](/sdk/remote-ui#config-cache-behavior). ## New features **Integration diagnostics** The **Integration** page shows whether the app is connected to PNLight. It also collects the latest install, attribution, event, and Remote UI signals. You can copy the project SDK token from the same page. [Read the docs](/dashboard/sdk-status). **Cohorts analytics** The Analytics section now includes a **Cohorts** view that groups users by registration window and tracks the revenue they generate over time. Use it to judge whether a paid acquisition cohort pays back its spend. [Read the docs](/dashboard/analytics#cohorts). **Remote UI analytics** The new **Remote UI** view in Analytics shows how many unique users requested a placement and how many saw its configuration over the selected period. [Read the docs](/dashboard/analytics#remote-ui-analytics). **Global dashboard search** Search across projects and pages from anywhere in the dashboard, and switch projects without losing your place. ## Updates **Adaptive chart granularity** Analytics charts now adjust their time granularity automatically based on the selected date range, with stricter date-range and view-by limits to keep results readable. **SDK version reported on events** The SDK now sends its version with every event, making it easier to correlate behavior with a specific SDK release when reviewing data. **Project member settings access** Project members (not just owners) can now edit project settings, so teams no longer need to route routine changes through a single account. ## Fixes **Remote UI internal attribution** Resolved an issue where internal attribution did not apply correctly to Remote UI sessions. Attribution now resolves consistently across SDK and Remote UI flows. # Ad networks Source: https://docs.pnlight.app/dashboard/ad-networks Configure ad networks and accounts in PNLight to normalize attribution, sync Meta campaigns, send Meta Conversions API events, set Target URL defaults, and send mapped events to AppsFlyer. Ad networks organize your paid acquisition data in PNLight. Use them to group campaigns by network and account, normalize attribution names, and configure the delivery settings your paid traffic uses. ## How ad networks are structured PNLight uses three levels for paid acquisition setup: * **Network**: a provider such as Facebook or a custom traffic source. * **Account**: a Facebook setup for campaign sync, Conversions API event delivery, or both; or a custom account inside that network. * **Campaign**: a synced or manual campaign that appears under the selected network or account. Use this structure to keep attribution, postbacks, and reporting aligned across the dashboard. ## Before you start For a Facebook connection, prepare access in Meta before you add the account in PNLight. * You need admin access to the Meta business portfolio that owns the assets you connect. * You need ad account access and a Facebook Ad Account ID only when PNLight should import campaigns and spend through the Marketing API. * You need a **Target URL**. This is usually your app's store URL, but can be a landing page, another tracker, or another valid HTTPS destination. When the project has an App Store ID, PNLight prefills the App Store listing URL for new accounts. * You need a Meta dataset or pixel and its Conversions API token if you want PNLight to send events through Meta Conversions API. For AppsFlyer S2S event delivery, prepare: * Your production AppsFlyer iOS app ID in `id1234567890` format. * An AppsFlyer S2S Security Token from **Security Center** > **Tokens**. * The PNLight subscription lifecycle events you want to send and the AppsFlyer event names that should receive them. You can create a Conversions API-only Facebook account without a Facebook Ad Account ID. Add the Facebook Ad Account ID and Marketing API token later if you want to enable Marketing API campaign sync. PNLight uses the Data Source ID and Conversions API token only when you enable Conversions API event delivery. ## Connect a Facebook account Select a project and open **Networks**. Click **Connect Facebook**. Choose **Marketing API** to import campaigns and spend, **Conversions API** to send mapped events to Meta, or **Both**. You can add the other capability later from account settings. The choice guides setup; PNLight determines active capabilities from the saved credentials. Add **Account name** so your team can recognize the setup later. Add **Target URL**. PNLight prefills it from the project App Store ID when available. PNLight opens the Target URL after it records an ad click. It is often an App Store URL, but can also point to another valid HTTPS destination. Add **Facebook Ad Account ID** and **Facebook Access Token (Marketing API)** if you want PNLight to import campaigns and spend from Meta. Add **Data Source ID** and **Conversions API Token** if you want PNLight to send mapped events to Meta. When you enable Conversions API, review which PNLight events map to Meta standard events, then copy the generated click URL template into your ads. PNLight needs the tracked click to attribute an event to the account and campaign before it can send the event to Meta. Save the account. PNLight validates the credentials you entered and creates the tracked click URL. If Marketing API credentials are present, PNLight also imports campaigns and spend. Campaigns can use the account **Target URL**, or set their own campaign Target URL when one campaign needs a different destination. In the campaign form, that field is labeled **Redirect URL override**. ### Get the Facebook credentials The credentials you need depend on what the account should do. | Capability | Required Meta values | What PNLight uses them for | | --------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------ | | Marketing API | Facebook Ad Account ID and Facebook Access Token (Marketing API) | Import campaigns, campaign structure, and spend from Meta. | | Conversions API | Data Source ID and Conversions API Token | Send mapped attributed conversion events to your Meta data source. | Every Facebook account also needs a **Target URL**. Use **Both** when the same Facebook setup should import campaign data and send conversion events. #### Get the ad account ID You need this value for Marketing API campaign sync. It is optional for a Conversions API-only account. Open the ad account that runs the campaigns you want to sync. Copy the numeric ad account ID from the account selector or account settings. You can paste the ID as `act_123456789` or `123456789`. #### Get the Marketing API access token Create a system user token for PNLight. Do not use a short-lived Graph API Explorer token. Open **Meta Business Settings** and select the business portfolio that owns the ad account. Open **Users** > **System users** and click **Add**. Name the user `PNLight` or another clear service name. Select the system user, click **Add assets**, choose **Ad accounts**, and select the ad account you will add to PNLight. Grant access that allows PNLight to read campaigns, ad sets, ads, and insights. Click **Generate new token** and select the Meta app that belongs to the same business portfolio. If you do not have an app yet, create one in Meta for Developers and connect it to this business portfolio. Select `ads_management`. Select `ads_read` too if Meta shows it for your app. Generate the token and copy it immediately. Paste it into **Facebook Access Token (Marketing API)** in PNLight. Meta reference: [Marketing API authorization](https://developers.facebook.com/docs/marketing-api/overview/authorization/). #### Get the Data Source ID and Conversions API token Use these values only when you want PNLight to send mapped events to Meta Conversions API. They must be added together. Open the dataset or pixel that should receive PNLight events. Copy the pixel or dataset ID from the data source details. PNLight labels this value **Data Source ID**. Open the Conversions API settings for the data source and generate an access token. Paste the ID into **Data Source ID** and the token into **Conversions API Token** in PNLight. Meta reference: [Conversions API get started](https://developers.facebook.com/docs/marketing-api/conversions-api/get-started/). ### Event mapping PNLight sends Conversions API events only when a PNLight event is mapped to a Meta standard event. PNLight starts new Facebook Conversions API accounts with these mappings: | PNLight event | Meta standard event | | ---------------------- | ------------------- | | `install` | `Lead` | | `trial_started` | `StartTrial` | | `subscription_started` | `Subscribe` | | `trial_converted` | `Subscribe` | | `trial_active` | `StartTrial` | Review these mappings before you continue. You can add mappings, remove mappings, or change the Meta event for each PNLight event. You can also change mappings later from the account settings or override them for a campaign when one campaign needs different event delivery. ### Click URL When you enable Conversions API, PNLight gives you a tracked click URL for the Facebook account. ```text theme={null} https://your-dashboard-domain/api/v1/ads/click?ad_account_id=ACCOUNT_ID&campaign_id={{campaign.id}}&adset_id={{adset.id}}&ad_id={{ad.id}} ``` Use this URL as the destination URL in Meta ads. PNLight records the click, keeps the Meta campaign values, and redirects the user to the account or campaign **Target URL**. Without a tracked click, PNLight cannot attribute a later event to the account and campaign, so it skips Conversions API delivery. For a Conversions API-only setup, the tracked click URL lets PNLight associate later events with the clicked Meta campaign even when Marketing API campaign import is not enabled. ## Connect AppsFlyer S2S events Use AppsFlyer S2S events when you want PNLight to send selected subscription lifecycle events to AppsFlyer for users with AppsFlyer attribution. Select a project, open **Integration**, and click **Configure** on **AppsFlyer S2S events**. Add **AppsFlyer App ID** and **S2S Security Token**. PNLight stores the token securely and does not show it again after you save it. Add one row for each PNLight subscription lifecycle event you want to send. Choose the **Platform event** and enter the **AppsFlyer event** name that should appear in AppsFlyer. Keep **Enabled** on when the credentials and mappings are ready. Turn it off to pause delivery without deleting the settings. PNLight starts with common mappings such as `trial_started`, `trial_converted`, `subscription_started`, `subscription_renewed`, and `subscription_refunded`. You can add, rename, or remove mappings before saving. AppsFlyer S2S delivery requires the app to send AppsFlyer attribution with an AppsFlyer user ID (AppsFlyer UID) through the SDK. Events can show **Waiting for attribution** until PNLight receives that identifier. ## Add a custom network or account Use a custom setup when the traffic source is not connected through Facebook sync. In **Networks**, click **Add network** and enter the network name and key. Add attribution aliases such as provider-specific names or normalized labels that PNLight should treat as the same network. Open the network row and click **Add account**. Add **Account name**, optional **External account ID**, and **Target URL**. PNLight prefills the Target URL from the project App Store ID when available. ## Configure network settings Open a network from the table when you need to manage shared delivery settings. Use the network page to: * Rename the network. * Update attribution network aliases. * Define default system event postbacks. * Define default app event postbacks. * Map built-in PNLight events to Facebook standard events when you use Facebook Conversions API. Facebook event mapping applies to built-in PNLight events, such as `install`, `trial_started`, and `subscription_started`. For custom SDK events sent with `logEvent`, use app event postbacks. ## Configure account settings Open an account when you need account-specific click and delivery settings. Use the account page to: * Rename the account. * Set or change the account Target URL (**Default redirect URL** for custom accounts). Campaigns use it when they do not have their own Target URL. * Copy **URL for your Ad** for Facebook campaign destinations. * Add or change the **Facebook Ad Account ID** before enabling Marketing API. * Update the Marketing API access token. * Update the Data Source ID and Conversions API token. * Turn **Send events to Facebook Conversions API** on or off. * Review **Conversions API delivery** for delivered, failed, and skipped events from the last 48 hours. ## Review sync health The **Networks** table shows a warning icon next to an account when the latest campaign or spend sync failed. Hover the icon to read the latest sync error. Use the warning to decide whether you need to refresh the Marketing API token, update Meta permissions, or check that the ad account is still accessible. For Facebook accounts, copy the generated **URL for your Ad** exactly as shown and use it as the destination URL in Meta. The URL includes Meta macros for campaign, ad set, ad, placement, and site source values. PNLight preserves that click context for reporting and postbacks. ## After setup * Open [Campaigns](/dashboard/campaigns) and sync or review the campaigns for that account. A campaign can optionally set its own Target URL; in the campaign form, this field is labeled **Redirect URL override**. When it is empty, PNLight uses the account Target URL. * Use ad network and ad campaign filters across **Analytics**, **Customers**, and **Events**. * Keep attribution aliases current when your provider sends a new network label. * Turn on Conversions API only after the Data Source credentials and event mapping are ready. The account settings show Conversions API delivery outcomes from the latest 48 hours; use **Events** to inspect individual attempts. * Open **Events** or a customer profile to check AppsFlyer S2S delivery status for mapped events. Use **Campaigns** after you connect an account to review enabled campaigns, costs, postbacks, and campaign metrics. # Analytics dashboard Source: https://docs.pnlight.app/dashboard/analytics Review installs, revenue, trials, subscriptions, ARPPU, rebills, cohorts, App Events, Remote UI, and funnels for your app in the PNLight dashboard. Use Analytics to understand how your app performs once the SDK and App Store integration are connected. ## Analytics sections | Section | What it shows | | ------------------- | --------------------------------------------------------------------- | | Overview | High-level app performance for the selected date range. | | Clicks | Acquisition click activity. | | Installs | App installs recorded by PNLight. | | Revenue | Revenue from purchases and subscriptions. | | Spend | Acquisition spend. | | New trials | Trial starts during the selected period. | | New subscriptions | New paid subscriptions. | | Trial cancellations | Trials where renewal was canceled. | | Trial conversions | Trials that converted to paid access. | | ARPPU | Cohort-based average revenue per paying user. | | Rebills | Subscription renewals after the first payment. | | Cohorts | Revenue and ROAS for users registered in a chosen window. | | App Events | Event volume, users, trends, and recent event samples for SDK events. | | Remote UI | Requests and shows over time for a Remote UI placement. | | Funnel | Conversion steps across the app and purchase flow. | ## Use filters Use **Date range**, **Ad network**, **Account**, and **Ad campaign** to answer specific questions: * What happened after a campaign launch? * Which ad accounts or campaigns drive paid subscriptions? * Did a Remote UI change affect subscription conversion? * Which in-app events changed after a release? * Are trial starts converting into rebills? Date filters include both selected dates. For example, a June 1-June 7 range includes activity through the end of June 7. **Account** limits the view to one or more ad accounts from **Ad networks**. The options follow the selected **Ad network**. Use **Unassigned** for traffic that is not tied to an ad account. The ARPPU, Rebills, and Cohorts sections also support a country filter, so you can isolate performance for a single geo. Compare analytics only after you confirm the SDK token, App Store integration settings, and campaign settings belong to the same project. For raw event debugging across all users, use [Events](/dashboard/events). Analytics focuses on trends and aggregates. ## Overview The Overview section summarizes acquisition, revenue, and subscription performance for the selected date range. Use it when you need a quick read on how users move from ads and installs into paid access. Open it from **Analytics → Overview**. ### Subscription funnels The **User Acquisition** group shows conversion funnels for the selected cohort: | Funnel | What it shows | | -------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Click to install | Ad clicks, installs, and the click-to-install conversion rate. | | Trial subscriptions | Installs, trial starts, trial conversions, renewals, and active subscriptions for users who started with a trial. | | Direct subscriptions | Installs, paid subscription starts, renewals, and active subscriptions for users who subscribed without a trial first. | PNLight shows the trial and direct subscription funnels only when the selected cohort has users in those paths. Each funnel can show renewals 1 through 5 when those events exist. Conversion percentages compare each step with the previous step, so you can spot where subscribers drop off after the first payment. On smaller screens, PNLight keeps the funnel readable by showing the earlier renewal steps first. Use a wider screen when you want to compare later renewals side by side. ### Funnel metrics | Metric | Definition | | -------------------- | --------------------------------------------------------------------- | | Click to Install CR | Installs divided by clicks. | | Trial started | Users from the install cohort who started a trial. | | Trial converted | Trial users who converted to a paid subscription. | | Trial to Sub CR | Trial converted users divided by trial users. | | Subscription started | Users who started a paid subscription without first starting a trial. | | Renewal 1 through 5 | Users who completed the first through fifth subscription renewal. | | Active subscriptions | Users from the cohort whose latest subscription state is active. | ## ARPPU The ARPPU section shows cumulative average revenue per paying user for customers who registered in a selected window. Use it to see how a cohort's monetization matures after acquisition. Open it from **Analytics → ARPPU**. ### Filters * **Registrations** — selects the cohort. Date presets set inclusive windows such as 30 days, 3 months, 6 months, or 12 months. * **Purchases until** — chooses how long to keep counting revenue after the registration window closes. Follow-up presets add 1, 3, 6, or 12 months. * **Ad network**, **Account**, **Ad campaign**, and **Country** — restrict the cohort to a traffic source, ad account, or geo. * **View by** — buckets the chart and table by day, week, or month. Available options adapt to the selected span. * **Calculate using** — switches between **Sales** and **Proceeds**. Sales are gross revenue before commissions. Proceeds are net revenue after store and bank commissions. * **Payer base** — controls the ARPPU denominator. **Running** divides each period by payers counted so far. **Final** divides every period by all payers in the selected window. * **Refunds** — controls whether refunded revenue is subtracted. Refunded customers still count as paying users. ### Metrics ARPPU values are cumulative through each row's period. The caption below each value shows the change from the previous period. | Metric | Definition | | ---------------------------- | ---------------------------------------------------------------------------------------- | | Cohort size | Users registered in the selected registration window. | | Cumulative paying users | Cohort users who have paid at least once, counted in the period of their first purchase. | | Conversion | Cumulative paying users divided by cohort size. | | Cumulative Sales or Proceeds | Revenue collected from the cohort through that period, using the selected revenue basis. | | ARPPU | Cumulative revenue divided by the selected payer base. | When revenue stops changing, the chart marks the plateau so you can see when the cohort stopped producing new revenue. ## How reinstalls affect analytics When the same SDK user ID installs the app again, PNLight treats the latest recorded install or reinstall as the start of that user's current analytics period. Dashboard analytics, campaign reports, and cohorts count only events that happened on or after that latest install. Events from earlier installs remain available in [Events](/dashboard/events) and customer timelines for debugging, but they are not included in aggregated analytics for the user's current campaign or cohort. ## App Events analytics The App Events section charts SDK events sent with `logEvent`. Use it to compare event names, spot release regressions, and inspect recent payloads without leaving the dashboard. Open it from **Analytics → App Events**. ### Filters * **Date range** — restricts the chart and table to events recorded on the selected dates. * **View by** — controls the chart bucket size. * **Ad network**, **Account**, and **Ad campaign** — restrict events to a traffic source. * **Event names** — choose which events appear on the chart. ### Metrics | Metric | Definition | | --------------- | ------------------------------------------------------------------- | | Event count | Total events with that name in the selected period. | | % of events | Share of all selected-period events represented by that event name. | | Users | Unique users who sent the event. | | Events per user | Event count divided by users. | | Delta | Change compared with the previous equivalent period. | Click an event name to open recent event samples. Each sample shows the user ID, timestamp, payload, and a link to the customer profile. ## Remote UI analytics The Remote UI section plots unique-user **Requests** and **Shows** for a single placement over time. Use it to see how often a placement is requested by the SDK and how often a config could actually be shown. Open it from **Analytics → Remote UI**, or from the **Remote UI** section in the sidebar. ### Filters * **Placement** — choose which Remote UI placement to chart. The first placement is selected by default. * **Date range** — restricts the time window for the chart. ### Metrics | Metric | Definition | | -------- | ---------------------------------------------------------------------- | | Requests | Unique users who requested the placement in each bucket. | | Shows | Unique users who received a config that could be shown in each bucket. | If the project has no placements yet, the page prompts you to create one first. See [Remote UI placements](/dashboard/remote-ui-placements) for setup. ## Cohorts The Cohorts section groups users by registration window and tracks the revenue they generate over time. Use it to judge whether a paid acquisition cohort pays back its spend. Open it from **Analytics → Cohorts** in the dashboard. ### Filters * **Registration window** — pick the start and end dates for the cohort (the users registered in that range). * **Last payment date** — the cutoff used when summing revenue from the cohort. * **Country** — restrict the cohort to a single geo. * **Ad network** and **Account** — restrict the cohort to a traffic source or ad account. * **Ad campaign** — restrict the cohort to users acquired through a specific campaign, organic users, or all users. The registration window includes users registered on both the start and end dates. ### Summary metrics The summary card reports the following for the selected cohort: | Metric | Definition | | -------------------- | ------------------------------------------------------------------ | | Total users | All users registered in the cohort window. | | ARPPU | Average revenue per paying user (revenue divided by paying users). | | Revenue | Total revenue after store commission, up to the last payment date. | | ROAS | Return on ad spend, calculated as Revenue ÷ Spend. | | Autorenewal ON | Users whose subscription has autorenewal enabled. | | Active subscriptions | Users whose latest rebill succeeded. | | Spend | The ad spend used in the ROAS calculation. | When you select a specific campaign, **Spend** is auto-detected from that campaign for the registration window. You can override it by typing a value into the Spend field. ### Weekly ROAS chart The chart plots cumulative revenue per week. When Spend is set, it converts revenue into ROAS and adds a dashed breakeven line at `1.0x`: * The series is grey before breakeven and green after, so you can see at which week the cohort paid back. * If breakeven is not reached, the chart shows cumulative revenue only and the subtitle reads "Breakeven not yet reached." * If Spend is `0`, the chart falls back to plotting cumulative revenue. # App Store integration Source: https://docs.pnlight.app/dashboard/app-store-credentials Add App Store Connect issuer ID, shared secret, and in-app purchase key, then configure App Store Server Notifications for your iOS app. App Store integration lets PNLight validate iOS purchases and process App Store Server Notifications. Configure it for each PNLight project that receives production or sandbox iOS purchases. ## Prerequisites * App Store Connect access with the Admin or Account Holder role. * A PNLight project with the correct iOS bundle ID and App Store ID. * At least one in-app purchase or subscription created in App Store Connect. ## Open AppStore settings in PNLight In the project dashboard, open **Integration**. Scroll to the **AppStore** section. Copy the **AppStore Connect notification URL**. You will add it in App Store Connect after saving the credentials. ## Create an in-app purchase key The in-app purchase key is a `.p8` private key from App Store Connect. PNLight uses it to verify StoreKit 2 purchases, decode App Store Server Notifications, and recover subscription history. Sign in to App Store Connect and open **Users and Access**. Open **Integrations** and then **In-App Purchase**. Generate an in-app purchase key and download the `.p8` file. Copy the issuer ID from the same App Store Connect page. Apple lets you download the `.p8` key only once. Store it securely and do not rename the file before uploading it to PNLight. ## Save credentials in PNLight In **Integration → AppStore**, fill in: | Field | What to enter | | ----------------------- | -------------------------------------------------------------------------------------------------- | | Issuer ID | The issuer ID from **Users and Access → Integrations → In-App Purchase** in App Store Connect. | | App Store shared secret | The app-specific or primary shared secret used for legacy receipt validation and receipt recovery. | | In-App Purchase Key | The downloaded `.p8` in-app purchase key. Keep the original file name. | Click **Save AppStore settings** after entering the issuer ID and shared secret. Upload the `.p8` file with the **Upload \*.p8** button. ## Configure server notifications App Store Server Notifications let PNLight receive subscription renewals, cancellations, refunds, billing retry events, and other subscription lifecycle changes without waiting for the app to open. In **Integration → AppStore**, copy the **AppStore Connect notification URL**. In App Store Connect, open **My Apps** and select the iOS app that matches this PNLight project. Open **App Information** and find **App Store Server Notifications**. Paste the PNLight URL into both **Production Server URL** and **Sandbox Server URL**. If App Store Connect asks for a notification version, select **Version 2**. Save the App Store Connect settings. If the App Store Server Notifications section is missing in App Store Connect, create at least one in-app purchase or subscription for the app and check again. ## What to verify before testing * The `.p8` file belongs to the same App Store Connect account. * The bundle ID matches the app that includes the SDK. * The App Store ID matches the app listing. * The notification URL is copied from the same PNLight project. * The notification URL is saved for both production and sandbox. * App Store Server Notifications use version 2. App Store changes can take time to affect sandbox and production events. Test with a fresh sandbox purchase after saving the integration settings. # Campaigns Source: https://docs.pnlight.app/dashboard/campaigns Review user acquisition campaigns in PNLight, including ad spend, install costs, postbacks, and campaign-level sales, conversions, and cohort analytics. Campaigns show how your paid traffic performs. Create a manual campaign when its traffic source is not connected through an ad network account. ## Use the campaigns page Select a project and open **Campaigns**. Check campaign names, network or account data, and enabled status. Enter or review costs when spend data needs correction. Set postback behavior for campaigns that need event delivery. Filter the table with **Ad network**, **Account**, and **Ad campaign** when you need channel-level analysis. ## Manual campaigns Manual campaigns let you track traffic from ad networks, affiliate partners, newsletters, influencers, or any other source that can use a campaign URL. PNLight records the click, redirects the user to your destination URL, attributes users when the app reports the click, and sends postbacks for matched events. Manual campaigns are different from synced Facebook campaigns: | Area | Manual campaign | Synced Facebook campaign | | ----------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | Campaign creation | You create the campaign in PNLight. | PNLight syncs campaigns from Facebook. | | Spend | You enter costs manually or import a CSV file. | PNLight can sync spend from Facebook. | | Postbacks | You configure system and app event postback URLs on the campaign. | Campaigns can inherit postbacks and Facebook event mapping from the connected network account, or override them when needed. | | Tracking URL | PNLight generates a campaign-specific URL with `pnlAdsCampaignId`. | PNLight generates an account-level tracked click URL with Facebook macros. | Use [Ad networks](/dashboard/ad-networks) for Facebook setup. ## Review campaign performance The campaign table groups synced campaigns by network and account. Expand a campaign to review ad set and ad rows when the connected source provides that hierarchy. The table includes: * **Spend** from synced or manually entered costs. * **Clicks** from PNLight tracking and the connected network. * **Installs**, **Trials**, and **Subs** from SDK and purchase events. * **CPI**, **CPT**, and **CAC** acquisition cost metrics. * **Sales** for gross sales before store or bank commissions. * **ROI** for campaign return. Turn on **Hide zero rows** when you want to focus on campaigns, ad sets, or ads with activity in the selected date range. ## Create a manual campaign Select a project and open **Campaigns**. Click **New Campaign**. Add **Campaign Name**. Add **Start date** and **End date** when the campaign has a planned reporting window. Click **Create**. Open the campaign from the table. Enter **Redirect URL override**. This campaign-level setting is the campaign Target URL. PNLight sends the click there after it records it. Copy **URL for your Ad** and use it as the destination URL in your ad network or partner system. A manual campaign must have **Redirect URL override** before you use **URL for your Ad** in live traffic. This field sets the campaign Target URL. Without a Target URL, PNLight cannot send the click onward. You can leave **Start date** or **End date** blank for open-ended campaigns. Set only **Start date** for campaigns that are already running, or only **End date** when you know the cutoff date. ## Configure the ad URL The manual campaign URL includes `pnlAdsCampaignId`. PNLight uses that value to connect clicks, installs, events, costs, postbacks, and analytics to the campaign. Add the parameters your traffic source can provide to the generated URL. PNLight stores those parameters with the click and forwards them to the Target URL. ```text theme={null} https://pnlight.example/api/v1/ads/click?pnlAdsCampaignId=CAMPAIGN_ID&sub_id={click_id}&partner=network_a&placement={placement} ``` Use stable parameter names that you can reuse in postbacks, such as `sub_id`, `click_id`, `partner`, `creative`, `placement`, or `utm_campaign`. When PNLight redirects the user, it keeps the campaign URL parameters and adds `deep_link_value` with the PNLight click ID. Your app or deep-link handler can use that value when it needs to associate the install with the click. If your traffic source already appends tracking parameters, keep them on **URL for your Ad**. They become available for postback URL substitution. ## Configure postbacks Postbacks send event notifications from PNLight back to an ad network, affiliate platform, or internal tracking endpoint. PNLight sends postbacks only for users attributed to the campaign. Open a manual campaign and use the postback sections: | Section | Use it for | | --------------------------- | ------------------------------------------------------------------------------- | | **System Events Postbacks** | Built-in PNLight events, such as install, purchase, refund, and `trial_active`. | | **App Events Postbacks** | Custom SDK events sent from your app with `logEvent`. | For each postback, enter: | Field | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------- | | **Event name** | The PNLight event that should trigger the postback. For app events, use the exact event name sent by the SDK. | | **Postback Url** | The full postback endpoint URL. PNLight calls this URL when the event is recorded for an attributed user. | | **Single Use** | Sends the app event postback only once per user, campaign, and event name. Use it for one-time conversions. | ### Substitute parameters in postback URLs Use placeholders in query parameter values when the receiving system needs values from the original click URL. ```text theme={null} https://network.example/postback?click_id={{sub_id}}&event=purchase&pnl_click_id={{pnlClickId}} ``` PNLight replaces: | Placeholder | Value | | -------------------------------------------------------- | ------------------------------------------------------------------------------ | | `{{pnlClickId}}` | The PNLight click ID for the matched user. | | `{{sub_id}}`, `{{click_id}}`, or another click parameter | The value with the same name from the original campaign URL. | | AppsFlyer parameters | Values from the AppsFlyer attribution payload when AppsFlyer wins attribution. | Placeholders are replaced only inside query parameter values. Missing values stay unchanged in the final URL, so check the receiving system for unreplaced placeholders during testing. ## Inherit or override synced campaign delivery settings Synced Facebook campaigns can inherit delivery settings from the connected network account. This keeps postbacks and event mapping consistent across many campaigns. Facebook event mapping covers built-in PNLight events only. It does not convert custom SDK events sent with `logEvent` into Facebook standard events. Use app event postbacks for those events. Use **Override** on the campaign when one campaign needs a different setup for: * System event postbacks. * App event postbacks. * Facebook event mapping. * Target URL. ## Enter campaign costs manually Manual campaign costs power spend, CPI, CPT, CAC, ROI, ROAS, and cohort payback views. Enter costs after you create the campaign. Open **Campaigns** and select the manual campaign. Update **Start date** or **End date** if the campaign dates changed. Leave either field blank when the campaign should stay open-ended. Use the date picker in **Costs data** to choose the dates you want to edit. Fill the cost amount for each day. Use the copy action next to a day to fill future dates with the same amount. Enter a month total or full campaign total when you want PNLight to distribute spend across the selected dates. Save the form to update analytics and campaign metrics. You can also click **Import from CSV**. Use a two-column CSV file with `date,cost` rows and no header row. Dates must use `YYYY-MM-DD`, and costs must be decimal currency values. ```csv theme={null} 2026-06-01,125.50 2026-06-02,130.00 2026-06-03,118.75 ``` ## Verify the setup After you save the campaign, test the full flow before buying traffic: * Open **URL for your Ad** with sample parameters and confirm that it redirects to the campaign Target URL set in **Redirect URL override**. * Confirm the redirected URL includes the parameters you expect. * Install or open the app from the test flow and confirm the user appears in **Customers** with the campaign attribution. * Trigger one system event or app event that has a postback. * Open the customer profile and check whether the postback was sent. * Review **Campaigns**, **Analytics**, and **Cohorts** to confirm spend, installs, revenue, and ROAS use the campaign. ## Common checks * Manual campaigns use **URL for your Ad** with the correct `pnlAdsCampaignId`. * Synced campaigns come from the correct ad network and account. * Manual campaigns have a campaign Target URL in **Redirect URL override** before live traffic starts. * Disabled campaigns are excluded from filters and analytics where expected. * Costs match the reporting period you are reviewing. * Postback event names match the system event or SDK app event you want to send. * Event mappings are configured on the ad network or campaign when Facebook Conversions API depends on them. # Create a project Source: https://docs.pnlight.app/dashboard/create-project Create a PNLight project for your iOS app, enter the App Store identifiers, choose the preferred attribution source, configure the Trial Active delay, and optionally connect App Store Connect. A project represents one app in PNLight. Create it before you install the SDK. The dashboard home page is **My apps**. It lists every app you can access and shows **Revenue**, **Net profit**, **Installs**, and **Active subscriptions** for the selected date range. When you have more than two apps, it can also show **Revenue mix** and **Profit by app**. The project creation flow collects the app identifiers first. It then asks how PNLight should prioritize attribution when multiple sources are available. ## Create the project Open **My apps** and click **New App**. Add the app name, App Store ID, and bundle ID. Choose the source PNLight should prefer first. Select **AppsFlyer** if you want AppsFlyer conversion data to win when it is available. Select **PNLight Attribution** if you want PNLight click attribution to win first. Click **Create app**. PNLight creates the project and opens the App Store Connect step. Add App Store Connect credentials now, or skip this step and configure them later from [Integration](/dashboard/app-store-credentials). After the project is created, open [Integration](/dashboard/sdk-status) and copy the SDK token. ## Project fields | Field | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | App name | The name shown in the dashboard. | | Bundle ID | The iOS bundle identifier for the app. | | App Store ID | The App Store app identifier. PNLight also uses it to prefill the App Store listing as the Target URL when you add new Facebook or custom ad network accounts. | | Attribution preference | The source PNLight should prefer first when it receives multiple attribution sources. | | Trial Active delay | How long PNLight waits before sending the `trial_active` system event for trials that remain active. | ## Trial Active event delay PNLight can send a `trial_active` system event after a user starts an App Store trial and keeps auto-renew enabled. Use it when your ad network, BI tool, or postback receiver should count trials that stay active beyond the initial cancellation window. New projects use a one-hour delay by default. After creating a project, open **Settings → Trial Active** to choose a delay from 1 to 24 whole hours. PNLight sends `trial_active` only when the trial is still active at the scheduled time. The event appears on the [Events](/dashboard/events) page and follows the same AppsFlyer S2S and system event postback delivery as other subscription lifecycle events. ## Optional App Store Connect setup If you have App Store Connect credentials ready, add them during project creation: * Issuer ID. * App Store shared secret. * In-app purchase `.p8` key. You can skip this step during creation. Configure it later before testing purchases or App Store Server Notifications. Match the bundle ID and App Store ID to the exact app build that uses this project SDK token. # Customers Source: https://docs.pnlight.app/dashboard/customers Look up individual customers in PNLight to review their profile, subscription and purchase status, attribution source, event history, and outgoing postbacks. The **Customers** page shows user-level data from the SDK and connected integrations. ## Customer list Use the customer list to review: * User ID. * Subscription status. * Autorenewal status. * Total spent. * First seen date. * **Ad network**, **Account**, and **Ad campaign** filters. * Purchase-state filters. * Test user filters. Use **Date range** to include customers first seen on either selected date or any date between them. Use **Test users → Test only** when you want to isolate QA traffic from production users. ## Test users Open a customer profile when you need to mark one SDK user ID for testing. * **Add test user** marks the current SDK user ID as a test user. * **Remove from test users** restores normal targeting and reporting behavior for that user. * Test users are useful for Remote UI QA, release checks, and one-off purchase debugging. ## Customer details Open a customer profile to review the timeline for a single user. Use it when you need to answer questions such as: * Did the SDK create a user ID? * Did a purchase event arrive? * Was the user attributed to a source? * Which app events did the SDK send? * Did Remote UI capture protection affect this user? * Is this user currently matched to a test device? * Were postbacks sent for this user? The profile can also show attribution resolution, attribution debug payloads, and external attribution records. Use them when a campaign, network, ad group, or provider identifier does not match what you expected. ## Review attribution details Use the **Attribution** section to confirm how PNLight selected a source for the customer. * **Attributed** means PNLight selected one source for the customer. * **Waiting** means PNLight has source signals, but it is still waiting for the preferred source. * **No attribution** means no source could be confirmed. * **Needs review** means multiple sources conflict. The section can show the matched campaign, ad set, and ad when that data is available. It also lists each checked source, such as PNLight click attribution or AppsFlyer attribution. ## Review the customer timeline Use the timeline filters to focus on subscription events, app events, system events, or postbacks. For postbacks, the timeline can show: * Custom postback HTTP status and destination host. * Facebook Conversions API delivery status. * Google Data Manager delivery status. * Delivery result, such as `delivered`, `failed`, or `skipped`. * Skip or failure reason when PNLight has one. * Facebook Dataset ID and mapped event name for Facebook Conversions API attempts. Open the project [Events](/dashboard/events) page when you need the full delivery attempt details across users. Customer data depends on SDK initialization. If a user is missing, check that the app build uses the correct SDK token. # Events Source: https://docs.pnlight.app/dashboard/events Review project events across all users, including raw event details, attribution context, and delivery attempts. The **Events** page shows raw project events across all users. Use it to debug event delivery, attribution context, or a specific payload without opening customer profiles one by one. ## What the page shows Each row includes: * Event type. * Event name. * SDK user ID. * Event time in UTC and local time. * Integration delivery status. * Event value when the event carries revenue. The page includes subscription events, custom app events, and system events. ## Filters Use the filters at the top of the page to narrow the dataset: * **Date range** to limit the time window. PNLight includes events recorded on both selected dates. * **Ad network** to isolate a traffic source. * **Account** to isolate one or more ad accounts. Use **Unassigned** for traffic that is not tied to an ad account. * **Ad campaign** to isolate one campaign. * **Type** to focus on subscription, app, or system events. * **Country** to narrow events by user country. * **Event name** to focus on one or more event names. ## Delivery status The **Integrations** column summarizes what happened after PNLight recorded the event. | Status | Meaning | | ----------------------- | ----------------------------------------------------------- | | Delivered | The latest delivery attempts for this event succeeded. | | Delivery issues | At least one latest delivery attempt failed. | | Partial delivery | At least one destination delivered and another was skipped. | | Skipped | Delivery was intentionally skipped for the latest attempts. | | Queued / In progress | PNLight accepted the event and is still processing it. | | Processed | PNLight finished processing the event. | | Waiting for attribution | The event is waiting for attribution before delivery. | | No delivery needed | No downstream delivery was required for this event. | ## Open an event Click a row to open the event drawer. Use the drawer to review: * The SDK user ID and direct link to the customer profile. * Timestamp. * Country. * Attributed network and campaign. * Revenue value when present. * Delivery attempts, including destination, status, reason, HTTP status, and request URL. * The raw event payload. ## When to use Events instead of Analytics Use **Events** when you need raw debugging for one event or one delivery attempt. Use [Analytics](/dashboard/analytics) when you need aggregated charts, trend lines, and conversion metrics. # Products Source: https://docs.pnlight.app/dashboard/products Manage the in-app purchase product IDs that belong to a PNLight project. Use **Products** to keep this project's in-app purchase product IDs in one place. This page is useful when you want a clean project-level catalog of the products your iOS app uses. ## Add a product Select a project and open **Products**. Click **Add product**. Paste the exact App Store product ID, such as `com.example.app.premium_monthly`. Select **iOS** for the current PNLight mobile SDK setup. Save the form. ## Edit or delete a product Use the table actions to update a product ID or remove an entry you no longer use. Edit a product when: * The App Store product ID changed before release. * You added the wrong identifier to the project. * You want to keep the project catalog aligned with the app build you ship. Delete a product when the ID should no longer be associated with the project. ## Common checks * Use the exact product ID from App Store Connect. * Keep production product IDs in the production PNLight project. * Keep staging or QA product IDs in a separate PNLight project when you need separate reporting. * Add each subscription or in-app purchase product ID your app can use. This page does not create products in App Store Connect. It documents the product IDs that belong to the current PNLight project. # Remote Config Source: https://docs.pnlight.app/dashboard/remote-config Create typed PNLight Remote Config keys, targeted overrides, publish revisions, and keep app behavior configurable without an app release. Remote Config lets you manage typed app settings in PNLight and deliver them to the SDK for each app installation. Use it for non-sensitive settings such as feature flags, paywall copy, limits, variants, and rollout controls. Remote Config is not a secret store. Do not put API keys, credentials, shared secrets, or other sensitive values in Remote Config. ## How Remote Config works Remote Config has two parts: * **Base configuration**: the required catalog of keys and default values. * **Overrides**: targeted value changes that apply when an installation matches their conditions. The SDK fetches the published configuration for the current PNLight user. If a fetch fails, the app keeps using the previously active configuration or the local defaults provided during SDK initialization. ## Create the base configuration Open **Remote Config** in the dashboard and add every key your app can read. Supported value types: | Type | Use for | | ------------ | ------------------------------------------------- | | Boolean | Feature flags and on/off switches. | | String | Copy variants, IDs, labels, and simple modes. | | Number | Limits, prices, durations, and percentages. | | String array | Lists of enabled plans, countries, or variants. | | JSON | Structured values that should be edited together. | Keys must start with a letter. They can contain letters, numbers, and underscores. Keep keys flat and stable. A JSON value is replaced as a whole by an override; PNLight does not deep-merge nested JSON fields. ## Add overrides Use overrides when a segment should receive a different value from the base configuration. An override can target installation attributes such as: * Attributed campaign, including organic users. * Device family and model. * OS version. * SDK version. * IP country or App Store country. Each override can replace only keys declared in the base configuration. Matching overrides apply in the order shown in the dashboard. Drag overrides to change their order; when multiple matching overrides set the same key, the later matching value wins. ## Save and publish Remote Config changes start as a draft. Add or update base keys and overrides. Save the draft when the fields validate. Publish the draft when it is ready for SDKs. SDK requests resolve only the latest published revision. Unpublished draft changes do not affect apps. ## Restore a revision Use **Revision history** to review published versions. Restoring a revision replaces the current draft with that version. Publish the restored draft to make it live again. ## Coordinate with the app Before you publish a new key, confirm that the app has a typed getter and a safe fallback for that key. See [Remote Config SDK integration](/sdk/remote-config) for Swift, React Native, and Flutter examples. # Remote UI placements Source: https://docs.pnlight.app/dashboard/remote-ui-placements Create and manage PNLight Remote UI placements, configure per-locale variants, add runtime placeholders, set display rules, and control how remote screens appear in your app. A Remote UI placement is a named location in your app where PNLight can return a remote screen. The placement in **Remote UI** is the source of truth for SDK requests. On a fresh fetch, if the app requests a placement that does not exist, is disabled, or has no usable locale config, PNLight returns no UI for that request. ## Create or edit a placement Select the project and open **Remote UI**. Open an existing placement or create one with a stable placement ID such as `paywall`. Use a readable name that dashboard users can recognize. Add JSON configs for the locales your app requests, such as `en` or `en-US`. Define reusable values when the same JSON should use project-specific strings or product IDs. Turn on the placement when it is ready for app traffic. ## Placement controls | Control | Use | | ----------------------- | ------------------------------------------------------------------------ | | Enabled | Allows the placement to return configs to the SDK. | | Debug | Lets you test the placement without the normal production display rules. | | Show only once per user | Prevents the same user from seeing the placement repeatedly. | | Protection mode | Controls secure rendering and capture handling for the placement. | | Default locale | Fallback locale used when the requested locale does not have a config. | ## Preview and validate a config Use the editor preview before you enable a placement. * Select a locale to edit its JSON config. * Review the live preview next to the editor. * Switch device presets to check common iPhone sizes. * Use the preview event log to confirm render, click, and custom action callbacks. * For `pnlight.purchase` actions, use the preview simulator to test **Success**, **Fail**, and, in schema v4, **Cancel**. The preview does not start a real StoreKit transaction. * Fix validation messages before you save. The preview is a dashboard validation tool. Always test the placement in your app before shipping a release. ## Runtime placeholders Runtime placeholders let you reuse one JSON config while PNLight fills values before the SDK receives it. Insert a token in any JSON string value with `{{key}}`. Placeholder keys must start with a letter. They can contain letters, numbers, and underscores. | Source | Use | | ------- | ------------------------------------------------------------------------------------- | | Literal | Replaces the token with a saved string value. | | Product | Replaces the token with an App Store product ID from [Products](/dashboard/products). | For example, define a product placeholder with the key `monthlyProductId`. Then use `{{monthlyProductId}}` in a button action parameter. If a placement references a placeholder that cannot be resolved, PNLight does not return that config to the SDK. Check that every token has a matching placeholder and that product placeholders point to existing products. ### Protection modes | Mode | Use | | ------------------------------ | ----------------------------------------------------------------------------------------------- | | Off | Do not apply Remote UI capture protection. | | Secure only | Render the placement in secure mode when supported by the SDK wrapper. | | Secure and block after capture | Render in secure mode and stop returning the placement after capture is detected for that user. | Capture handling applies only when the placement uses a secure protection mode. Placements with protection mode **Off** remain visible during screen recording or screenshot attempts. ## Targeting conditions Targeting conditions restrict a placement to matching users. Add conditions when a placement should appear only for specific campaigns, devices, countries, or SDK versions. Available fields: * Ad campaign. * Device family. * Device model name. * OS version. * IP country. * Store country. * SDK version. Each condition can include or exclude values. Numeric or version-like fields can also use greater-than-or-equal and less-than-or-equal modes. Use **Debug enabled** or a test device when you need to verify a placement before the targeting conditions are ready for production. ## Metrics | Metric | Meaning | | -------- | ------------------------------------------------ | | Requests | Users who requested the placement. | | Shown | Users who received a config that could be shown. | For a time-series view of these metrics per placement, see [Remote UI analytics](/dashboard/analytics#remote-ui-analytics). Keep placement IDs stable. Changing a placement ID requires an app code change. Deleting a placement stops fresh SDK requests for that placement from returning UI. Disable the placement when you want to pause it without removing its setup. # Integration Source: https://docs.pnlight.app/dashboard/sdk-status Check whether the PNLight SDK is connected, see the latest installs, attribution, events, and Remote UI signals, manage test devices, and copy the SDK token from one dashboard page. The **Integration** page shows whether your app is talking to PNLight. Use it to copy the project SDK token, check that each signal arrives after integration, manage App Store settings, add test devices, and investigate analytics that does not look right. Open it from the project sidebar under **Integration**. ## Integration status The header card summarizes the connection at a glance. | State | Meaning | | --------------- | --------------------------------------------------------- | | Connected | The project received an SDK message in the last 48 hours. | | Needs attention | The most recent SDK message is older than 48 hours. | | Not connected | The project has never received an SDK message. | The card also shows the **SDK version** and **platform** (iOS, React Native, Flutter, or Swift Package Manager) reported by the most recent install, plus the timestamp of the last observed message across all signals. If the status is **Not connected** after you ship, double-check that the build uses the project SDK token shown on this page. ## SDK token The SDK token connects app SDK calls to this PNLight project. Select the project in the dashboard. In the project sidebar, open **Integration**. Copy the token shown in the header card. Pass the token to the SDK initialization method. ```swift Swift theme={null} await PNLightSDK.shared.initialize(apiKey: "pnlight_sdk_token") ``` ```tsx React Native theme={null} await initialize("pnlight_sdk_token"); ``` ```dart Flutter theme={null} await PNLightSDK.initialize('pnlight_sdk_token'); ``` ## Integration guide Shortcut buttons link out to the per-platform setup pages: * [iOS Swift](/sdk/ios-swift) * [React Native](/sdk/react-native) * [Flutter](/sdk/flutter) ## App Store integration The Integration page includes the **AppStore** section for iOS purchase and subscription reporting. Keep these settings aligned with the app that uses the SDK token. Use this section to: * Copy the App Store Server Notifications URL. * Save the App Store Connect issuer ID. * Save the App Store shared secret. * Upload the `.p8` in-app purchase key. After saving the credentials, paste the PNLight notification URL into both the production and sandbox server notification fields in App Store Connect. See [App Store integration](/dashboard/app-store-credentials) for the full iOS setup. ## Test devices Add test device IDFAs or SDK user IDs when you need a device to always receive Remote UI in test mode. Test devices can receive Remote UI even when normal targeting or capture protection would block it. Use test devices for QA builds, local validation, and release checks. You can also add or remove a test user from an individual customer profile when you want to debug one SDK user ID. Disable or remove entries when testing is complete. ## Signals A stepper lists each signal the SDK can send. Each row shows whether PNLight has received that signal, how many messages have arrived, and when the latest one was recorded. | Signal | What it means | | ------------ | --------------------------------------------------------------------- | | Installs | The app called the install/initialization endpoint. | | Attributions | The app forwarded attribution data from a provider such as AppsFlyer. | | Events | The app sent a custom in-app event. | | Remote UI | The app requested a Remote UI placement and recorded an impression. | A signal is flagged **Needs attention** if its latest message is older than 48 hours, even when other signals are healthy. Use this to spot integrations that regressed after a release — for example, attribution stops flowing while installs keep arriving. ## Common checks If a signal stays **Not received** after release: 1. Confirm the app build is using this project's SDK token. 2. Make sure the relevant SDK call runs on a real device after SDK initialization. 3. For attribution, verify the attribution provider returns data and the app forwards it to the SDK. 4. For Remote UI, confirm a placement exists in [Remote UI placements](/dashboard/remote-ui-placements) and the app requests it. For deeper debugging, see [SDK setup issues](/troubleshooting/sdk-setup). # Team Source: https://docs.pnlight.app/dashboard/team Invite teammates to your PNLight project, manage member access, and assign roles for people who configure the SDK, edit settings, or review analytics. Use **Team** to choose who can access a PNLight project. ## Add a team member Select a project and open **Team**. Click **New Member**. Enter the teammate **Email**. Click **Add**. PNLight emails that person with a link to the project. If they do not have an account yet, they can sign up from the email. ## Remove a team member Remove access when a teammate no longer works on the app or should not see the project. ## Access guidance * Give developers access while they integrate SDK and App Store settings. * Give product and marketing teammates access when they manage Remote UI or review analytics. * Review access before launches and after team changes. # Welcome to PNLight Source: https://docs.pnlight.app/index Connect PNLight to your iOS, React Native, or Flutter app. Track installs, send attribution, use Remote Config and Remote UI, and review your app analytics. Start here to create your project, install the SDK, connect the App Store, and check that customer, event, and revenue data is reaching the dashboard. Create a project, copy the SDK token, configure App Store integration, and initialize the SDK. Choose Swift, React Native, or Flutter and add PNLight to your app. Show screens you configure in the dashboard, including paywalls, prompts, and onboarding steps. Fetch typed feature flags, copy variants, limits, and other settings for your app. Review clicks, installs, revenue, subscriptions, trials, ARPPU, rebills, and funnel data. ## What you set up Add your app in the dashboard and enter the app identifiers used by the App Store. Add the SDK package for your app stack and initialize it with the project SDK token. If you use an attribution provider, send its data to PNLight. Publish typed app settings, create placements, and publish localized UI configs. Then use the dashboard to review performance. ## Common paths * New integration: start with the [quickstart](/start/quickstart). * Native iOS app: use the [Swift SDK guide](/sdk/ios-swift). * React Native app: use the [React Native SDK guide](/sdk/react-native). * Flutter app: use the [Flutter SDK guide](/sdk/flutter). * Runtime app settings: use [Remote Config](/sdk/remote-config). * Dashboard owner: start with [create a project](/dashboard/create-project) and [analytics](/dashboard/analytics). # Attribution Source: https://docs.pnlight.app/sdk/attribution Forward attribution data from AppsFlyer or another mobile attribution provider into PNLight so installs, Remote UI requests, and campaigns are linked correctly. Attribution connects a user with the channel or campaign that brought them to your app. ## How attribution works in your app Your attribution provider, such as AppsFlyer, Firebase, or Facebook, resolves the attribution data. Send it to PNLight with the SDK after the provider returns conversion data. PNLight uses the project attribution settings to decide which source has priority when more than one source can describe the same user. ## Recommended AppsFlyer flow Launch the campaign in AppsFlyer and use AppsFlyer links for user acquisition. When a user opens the campaign link, AppsFlyer creates a unique ID for that user. After the app is installed, request App Tracking Transparency permission when your app needs IDFA-based attribution. If PNLight initialized before the ATT response, call `updateIdfa()` after authorization. Initialize the AppsFlyer SDK after the ATT response. Optionally set the PNLight user ID in your attribution tools, such as `appsFlyer.setCustomerUserId`, so the same user ID appears across tools. Read the AppsFlyer conversion result and send it to PNLight with `addAttribution`. On the first app launch, wait 4-5 seconds after AppsFlyer initialization before calling `getUIConfig` or showing `RemoteUiView`. When `attributionRequired` is `true`, the SDK also waits for attribution internally. ## Send attribution ```swift Swift theme={null} await PNLightSDK.shared.addAttribution( provider: .appsFlyer, data: ["af_status": "Non-organic"], identifier: "appsFlyerCustomerId" ) await PNLightSDK.shared.updateIdfa() ``` ```tsx React Native theme={null} import { addAttribution, updateIdfa } from "@pnlight/sdk-react-native"; await addAttribution( "appsFlyer", { af_status: "Non-organic" }, "appsFlyerCustomerId" ); await updateIdfa(); ``` If you enable AppsFlyer S2S events in the dashboard, pass the AppsFlyer user ID (AppsFlyer UID) as the attribution `identifier`. PNLight uses it to send mapped subscription lifecycle events back to AppsFlyer. ## AppsFlyer examples ```swift Swift theme={null} import AppTrackingTransparency import AppsFlyerLib import PNLightSDK import UIKit final class AppDelegate: NSObject, UIApplicationDelegate, AppsFlyerLibDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { AppsFlyerLib.shared().appsFlyerDevKey = "your-appsflyer-dev-key" AppsFlyerLib.shared().appleAppID = "your-ios-app-id" AppsFlyerLib.shared().delegate = self Task { await PNLightSDK.shared.initialize(apiKey: "pnlight_sdk_token") await ATTrackingManager.requestTrackingAuthorization() await PNLightSDK.shared.updateIdfa() AppsFlyerLib.shared().customerUserID = PNLightSDK.shared.getUserId() AppsFlyerLib.shared().start() } return true } func onConversionDataSuccess(_ installData: [AnyHashable: Any]) { let data = Dictionary(uniqueKeysWithValues: installData.compactMap { key, value in guard let key = key as? String else { return nil } return (key, value) }) Task { await PNLightSDK.shared.addAttribution( provider: .appsFlyer, data: data, identifier: AppsFlyerLib.shared().getAppsFlyerUID() ) } } func onConversionDataFail(_ error: Error) { print("AppsFlyer conversion data error:", error) } } ``` ```tsx React Native theme={null} import appsFlyer from "react-native-appsflyer"; import { requestTrackingPermission } from "react-native-tracking-transparency"; import { addAttribution, getUserId, initialize, updateIdfa, } from "@pnlight/sdk-react-native"; const getAppsFlyerUID = () => new Promise((resolve) => { appsFlyer.getAppsFlyerUID((error, uid) => { if (error) { console.error("AppsFlyer UID error", error); resolve(null); return; } resolve(uid); }); }); appsFlyer.onInstallConversionData(async (conversionData) => { const appsFlyerId = await getAppsFlyerUID(); await addAttribution( "appsFlyer", conversionData?.data ?? conversionData, appsFlyerId ); }); await initialize("pnlight_sdk_token"); await requestTrackingPermission(); await updateIdfa(); appsFlyer.initSdk({ devKey: "your-appsflyer-dev-key", appId: "your-ios-app-id", isDebug: false, onInstallConversionDataListener: true, }, async () => { const pnlightUserId = await getUserId(); appsFlyer.setCustomerUserId(pnlightUserId); }); ``` ```dart Flutter theme={null} import 'package:app_tracking_transparency/app_tracking_transparency.dart'; import 'package:appsflyer_sdk/appsflyer_sdk.dart'; import 'package:pnlight_sdk/pnlight_sdk.dart'; final appsFlyerOptions = AppsFlyerOptions( afDevKey: 'your-appsflyer-dev-key', appId: 'your-ios-app-id', showDebug: false, ); final appsFlyerSdk = AppsflyerSdk(appsFlyerOptions); await PNLightSDK.initialize('pnlight_sdk_token'); await AppTrackingTransparency.requestTrackingAuthorization(); await PNLightSDK.updateIdfa(); appsFlyerSdk.onInstallConversionData((conversionData) async { final appsFlyerId = await appsFlyerSdk.getAppsFlyerUID(); final payload = conversionData['payload'] ?? conversionData; await PNLightSDK.addAttribution( provider: 'appsFlyer', data: Map.from(payload as Map), identifier: appsFlyerId, ); }); await appsFlyerSdk.initSdk( registerConversionDataCallback: true, registerOnAppOpenAttributionCallback: false, registerOnDeepLinkingCallback: false, ); ``` On the first launch, AppsFlyer attribution usually accounts for most of the wait before a Remote UI request. When attribution is required, wait 4-5 seconds after AppsFlyer initialization. ## Supported provider names | SDK | Provider values | | ------------ | ----------------------------------------- | | Swift | `.appsFlyer`, `.firebase`, `.facebook` | | React Native | `"appsFlyer"`, `"firebase"`, `"facebook"` | | Flutter | `"appsFlyer"`, `"firebase"`, `"facebook"` | ## Dashboard settings Open **Settings** and configure the attribution priority: * Put the preferred source first. * Keep at least one source selected. * Set the wait window for preferred attribution. A longer wait window can improve attribution completeness. It can also delay early decisions while PNLight waits for the preferred source to respond. If the preferred source reports no attribution, PNLight can continue to the next enabled source without waiting for the full window. # Events Source: https://docs.pnlight.app/sdk/events Send optional in-app events such as onboarding completion, paywall views, and product interactions from your app to PNLight. Use `logEvent` to send optional in-app events to PNLight, much like you send app events to AppsFlyer. You can find them in **Analytics → App Events**, on the project **Events** page, and in customer timelines. ## Event names Use stable, lowercase event names. Keep names specific enough to understand in reports. Good examples: * `onboarding_completed` * `paywall_viewed` * `trial_started` Avoid names that include user-specific values, timestamps, or build numbers. ## Send an event ```swift Swift theme={null} await PNLightSDK.shared.logEvent("paywall_viewed", eventArgs: [ "placement": "paywall", "source": "onboarding" ]) ``` ```tsx React Native theme={null} import { logEvent } from "@pnlight/sdk-react-native"; await logEvent("paywall_viewed", { placement: "paywall", source: "onboarding", }); ``` ```dart Flutter theme={null} await PNLightSDK.logEvent('paywall_viewed', { 'placement': 'paywall', 'source': 'onboarding', }); ``` ## Event parameters Add parameters when they give you useful context for segmenting or debugging an event. | Parameter | Example | Use | | ----------- | ------------ | ------------------------------------------- | | `placement` | `paywall` | Connect UI actions to Remote UI placements. | | `source` | `onboarding` | Record where the event happened. | Keep event payloads small. Use event parameters for reporting context, not as a replacement for your app database. ## Review events in the dashboard Open **Analytics → App Events** to chart event volume over time, compare event names, and inspect recent event samples. Open **Events** when you need raw delivery status, payloads, or cross-user debugging. # Flutter SDK Source: https://docs.pnlight.app/sdk/flutter Install the pnlight_sdk Flutter package, configure the iOS Podfile and Info.plist, and initialize the PNLight SDK in your Flutter app for iOS. Use `pnlight_sdk` to add PNLight to a Flutter app on iOS. ## Requirements * Flutter 3.3.0+ * Dart 2.17.0 or later, below 4.0.0 * iOS 12.0+ ## Install Add the package: ```bash theme={null} flutter pub add pnlight_sdk ``` Package page: [pnlight\_sdk on pub.dev](https://pub.dev/packages/pnlight_sdk). [![Flutter SDK version](https://img.shields.io/pub/v/pnlight_sdk?label=Flutter)](https://pub.dev/packages/pnlight_sdk) ## iOS setup If you use `RemoteUiView`, add the DivKit CocoaPods source to `ios/Podfile` before other sources: ```ruby theme={null} source 'https://github.com/divkit/divkit-ios.git' ``` If your app collects IDFA on iOS 14.0+, add `NSUserTrackingUsageDescription` to `Info.plist`: ```xml theme={null} NSUserTrackingUsageDescription This app uses device tracking to support attribution and analytics. ``` ## Initialize the SDK ```dart theme={null} import 'package:pnlight_sdk/pnlight_sdk.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await PNLightSDK.initialize('pnlight_sdk_token'); runApp(const MyApp()); } ``` If you use Remote Config, provide local defaults during initialization: ```dart theme={null} await PNLightSDK.initialize( 'pnlight_sdk_token', remoteConfigDefaults: { 'paywall_enabled': false, 'welcome_title': 'Welcome', }, ); ``` ## Common calls ```dart theme={null} final success = await PNLightSDK.addAttribution( provider: 'appsFlyer', data: {'af_status': 'Non-organic'}, identifier: 'appsFlyerCustomerId', ); await PNLightSDK.logEvent('paywall_viewed', { 'placement': 'paywall', }); ``` `logEvent` sends optional in-app events to PNLight. You can review them in **Analytics → App Events**, on the **Events** page, and in customer timelines. ## Remote Config Fetch Remote Config after initialization. Read values with typed getters and fallbacks your app owns. ```dart theme={null} final result = await PNLightSDK.fetchAndActivate(); final paywallEnabled = await PNLightSDK.remoteConfigBoolean( 'paywall_enabled', fallback: false, ); ``` By default, `fetchAndActivate` waits briefly for AppsFlyer attribution and uses a 15-minute minimum fetch interval. See [Remote Config](/sdk/remote-config) for fetch results, local defaults, and all supported value types. ## IDFA after ATT If PNLight initializes before the App Tracking Transparency prompt completes, call `updateIdfa()` after authorization so PNLight receives the granted IDFA. ```dart theme={null} import 'package:app_tracking_transparency/app_tracking_transparency.dart'; import 'package:pnlight_sdk/pnlight_sdk.dart'; await AppTrackingTransparency.requestTrackingAuthorization(); await PNLightSDK.updateIdfa(); ``` ## Remote UI ```dart theme={null} import 'package:pnlight_sdk/pnlight_sdk.dart'; RemoteUiView( placement: 'paywall', cardId: 'paywall_card', ignoreCache: false, onPurchased: (String productId) async { await refreshEntitlements(productId); openMainScreen(); }, onAction: (RemoteUiActionEvent event) async { if (event.logId == 'close_button') { closePaywall(); } }, onError: (Object error) { // Show a local fallback or close the screen. }, ) ``` Use `onPurchased` when a schema v3 or v4 Remote UI document starts a purchase with `pnlight.purchase` and the app needs to refresh entitlements after StoreKit verifies it. The Flutter package wraps the PNLight iOS SDK. Do not call PNLight SDK methods on unsupported platforms. # iOS Swift SDK Source: https://docs.pnlight.app/sdk/ios-swift Add the PNLight Swift SDK to a native iOS app with Swift Package Manager and initialize it at startup to track installs, send attribution, fetch Remote Config, and render Remote UI. Use the Swift SDK to add PNLight to a native iOS app. ## Requirements * iOS 13.0+ * Swift 5.7+ * Swift Package Manager ## Install with Swift Package Manager Package repository: [pnlight-dev/sdk-swift](https://github.com/pnlight-dev/sdk-swift). [![Swift SDK version](https://img.shields.io/github/v/tag/pnlight-dev/sdk-swift?label=Swift%20SDK\&sort=semver)](https://github.com/pnlight-dev/sdk-swift/tags) Add it in Xcode: In Xcode, click **File** and then **Add Packages...**. Enter `https://github.com/pnlight-dev/sdk-swift.git`. Add the package to your app target. ## Initialize the SDK Initialize the SDK during app startup. ```swift theme={null} import PNLightSDK await PNLightSDK.shared.initialize(apiKey: "pnlight_sdk_token") ``` If you use Remote Config, provide local defaults during initialization: ```swift theme={null} let config = PNLightConfig(remoteConfigDefaults: [ "paywall_enabled": .boolean(false), "welcome_title": .string("Welcome"), ]) await PNLightSDK.shared.initialize(apiKey: "pnlight_sdk_token", config: config) ``` ## Common calls ```swift theme={null} let userId = PNLightSDK.shared.getUserId() await PNLightSDK.shared.addAttribution( provider: .appsFlyer, data: ["af_status": "Non-organic"], identifier: "appsFlyerCustomerId" ) await PNLightSDK.shared.logEvent("paywall_viewed", eventArgs: [ "placement": "paywall" ]) ``` `logEvent` sends optional in-app events to PNLight. You can review them in **Analytics → App Events**, on the **Events** page, and in customer timelines. ## Remote Config Fetch Remote Config after initialization. Read values with typed getters and fallbacks your app owns. ```swift theme={null} let result = await PNLightSDK.shared.fetchAndActivate() let paywallEnabled = PNLightSDK.shared.remoteConfigBoolean( forKey: "paywall_enabled", fallback: false ) ``` By default, `fetchAndActivate` waits briefly for AppsFlyer attribution and uses a 15-minute minimum fetch interval. See [Remote Config](/sdk/remote-config) for fetch results, local defaults, and all supported value types. ## IDFA after ATT If PNLight initializes before the App Tracking Transparency prompt completes, call `updateIdfa()` after authorization so PNLight receives the granted IDFA. ```swift theme={null} import AppTrackingTransparency import PNLightSDK await ATTrackingManager.requestTrackingAuthorization() await PNLightSDK.shared.updateIdfa() ``` ## Remote UI Use `RemoteUiView` in SwiftUI when you want PNLight to render a placement you configured in the dashboard. ```swift theme={null} import PNLightSDK import SwiftUI struct PaywallScreen: View { var body: some View { RemoteUiView( placement: "paywall", cardId: "paywall_card", onPurchased: { productId in Task { await refreshEntitlements() unlockPurchasedContent(for: productId) } }, onAction: { action in if action.logId == "close_button" { closePaywall() } } ) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ``` If you need to fetch the placement config yourself, use `getUIConfig`. On the first app launch, wait 4-5 seconds after AppsFlyer initialization when attribution is required. The SDK also waits for attribution internally when `attributionRequired` is `true`. ```swift theme={null} try? await Task.sleep(nanoseconds: 5_000_000_000) let config = await PNLightSDK.shared.getUIConfig(placement: "paywall") ``` Use `getUIConfigResult` when your app needs to tell the difference between no UI for a placement and a loading error. ```swift theme={null} let result = await PNLightSDK.shared.getUIConfigResult( placement: "paywall", ignoreCache: true ) ``` Use `onPurchased` when a schema v3 or v4 Remote UI document starts a purchase with `pnlight.purchase` and the app needs to refresh entitlements after StoreKit verifies it. See [Remote UI](/sdk/remote-ui) for placement and action guidance. # SDK overview Source: https://docs.pnlight.app/sdk/overview Compare the PNLight Swift, React Native, and Flutter SDKs, pick the one for your app stack, and review the core features each SDK supports on iOS. Install PNLight so your app can identify users, send attribution and optional in-app events, fetch Remote Config values, and request Remote UI configs. ## Supported SDKs Native iOS apps. Requires iOS 13.0+ and Swift 5.7+. React Native iOS apps. Package: `@pnlight/sdk-react-native`. Flutter iOS apps. Package: `pnlight_sdk`. ## SDK package links Open the Swift Package Manager repository. ![Swift SDK version](https://img.shields.io/github/v/tag/pnlight-dev/sdk-swift?label=Swift%20SDK\&sort=semver) Open the npm package page. ![React Native SDK version](https://img.shields.io/npm/v/%40pnlight%2Fsdk-react-native?label=React%20Native) Open the pub.dev package page. ![Flutter SDK version](https://img.shields.io/pub/v/pnlight_sdk?label=Flutter) ## What the SDK does * Initializes PNLight with the project SDK token. * Creates or returns a stable user ID. * Reports first installs and repeat app opens. * Sends optional in-app events. * Sends attribution data from providers such as AppsFlyer. * Fetches typed Remote Config values. * Requests and renders Remote UI. The current public SDK packages are iOS-focused. If your app targets multiple platforms, gate PNLight calls so unsupported platforms do not execute iOS-only code. ## Recommended integration order Call initialization once during app startup. Read the SDK user ID and use it consistently with attribution and Remote UI flows. Send attribution data after AppsFlyer or another provider returns conversion data. If the app asks for App Tracking Transparency permission after PNLight initialization, call `updateIdfa()` after the prompt completes. Fetch typed app settings and read them with local fallbacks. Render a placement and handle custom actions from the returned UI. ## Remote Config and Remote UI Use [Remote Config](/sdk/remote-config) for values your app applies in code, such as feature flags, copy variants, limits, and paywall settings. Use [Remote UI](/sdk/remote-ui) when you want PNLight to return a screen config your app can render. # React Native SDK Source: https://docs.pnlight.app/sdk/react-native Install @pnlight/sdk-react-native, link the native iOS module, and initialize the PNLight SDK in a React Native app to track installs, send attribution, fetch Remote Config, and render Remote UI. Use `@pnlight/sdk-react-native` to add PNLight to a React Native app on iOS. ## Install ```bash theme={null} npm install @pnlight/sdk-react-native ``` Package page: [@pnlight/sdk-react-native on npm](https://www.npmjs.com/package/@pnlight/sdk-react-native). [![React Native SDK version](https://img.shields.io/npm/v/%40pnlight%2Fsdk-react-native?label=React%20Native)](https://www.npmjs.com/package/@pnlight/sdk-react-native) ## Initialize the SDK Call `initialize` once when your app starts. ```tsx theme={null} import { initialize } from "@pnlight/sdk-react-native"; await initialize("pnlight_sdk_token"); ``` If you use Remote Config, pass local defaults as the third argument: ```tsx theme={null} await initialize("pnlight_sdk_token", undefined, { paywall_enabled: false, welcome_title: "Welcome", }); ``` ## Common calls ```tsx theme={null} import { addAttribution, getUserId, logEvent, } from "@pnlight/sdk-react-native"; const userId = await getUserId(); await addAttribution( "appsFlyer", { af_status: "Non-organic" }, "appsFlyerCustomerId" ); await logEvent("paywall_viewed", { placement: "paywall", }); ``` `logEvent` sends optional in-app events to PNLight. You can review them in **Analytics → App Events**, on the **Events** page, and in customer timelines. ## Remote Config Fetch Remote Config after initialization. Read values with typed getters and fallbacks your app owns. ```tsx theme={null} import { fetchAndActivate, getRemoteConfigBoolean, } from "@pnlight/sdk-react-native"; try { await fetchAndActivate(); } catch { // Previously active values and local defaults remain available. } const paywallEnabled = await getRemoteConfigBoolean( "paywall_enabled", false ); ``` By default, `fetchAndActivate` waits briefly for AppsFlyer attribution and uses a 15-minute minimum fetch interval. See [Remote Config](/sdk/remote-config) for fetch results, local defaults, and all supported value types. ## IDFA after ATT If PNLight initializes before the App Tracking Transparency prompt completes, call `updateIdfa()` after authorization so PNLight receives the granted IDFA. ```tsx theme={null} import { updateIdfa } from "@pnlight/sdk-react-native"; import { requestTrackingPermission } from "react-native-tracking-transparency"; await requestTrackingPermission(); await updateIdfa(); ``` ## Remote UI component Use `RemoteUiView` to fetch and render a placement. Add an `onAction` handler so your app can respond to actions from that UI. ```tsx theme={null} import { RemoteUiView } from "@pnlight/sdk-react-native"; export function PaywallScreen() { return ( { refreshEntitlements(productId).then(() => { navigation.navigate("Main"); }); }} onAction={(event) => { if (event.logId === "close_button") { navigation.goBack(); } }} /> ); } ``` ### Remote UI props | Prop | Type | Description | | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------- | | `placement` | `string` | Placement ID configured in PNLight. | | `cardId` | `string` | Optional card ID. Defaults to a placement-based value. | | `secure` | `boolean` | Deprecated. Secure rendering is controlled by the dashboard placement. | | `preventRecording` | `boolean` | Deprecated. Capture blocking is controlled by the dashboard placement. | | `attributionRequired` | `boolean` | Waits for attribution before returning a config when enabled. Defaults to `true`. | | `ignoreCache` | `boolean` | Waits for a fresh server response instead of serving a cached config. Defaults to `false`. | | `onAction` | `function` | Handles custom actions from the rendered UI. | | `onPurchased` | `function` | Receives `{ productId }` after a verified purchase started by a schema v3 or v4 Remote UI `pnlight.purchase` action. | | `onError` | `function` | Handles config loading errors. | Use [Remote UI](/sdk/remote-ui) to learn how placements and actions work. # Remote Config Source: https://docs.pnlight.app/sdk/remote-config Fetch typed PNLight Remote Config values from Swift, React Native, and Flutter apps with local defaults and safe fallbacks. PNLight Remote Config delivers typed app settings from the dashboard to the SDK. Use it for non-sensitive runtime settings such as feature flags, copy variants, limits, and paywall settings. Remote Config and [Remote UI](/sdk/remote-ui) do different jobs. Remote UI returns screen configs your app can render. Remote Config returns typed values that your app reads and applies in its own code. Do not store secrets in Remote Config. Never put API keys, credentials, shared secrets, or other sensitive values in Remote Config. ## Configure values in the dashboard Create the base configuration, add targeted overrides, and publish the draft in **Remote Config**. See [Remote Config dashboard setup](/dashboard/remote-config) for the workflow. Supported top-level value types: * Boolean. * String. * Number. * String array. * JSON. Keys are flat. If an override changes a JSON value, it replaces the whole value instead of deep-merging nested fields. ## Initialize with local defaults Provide app-owned defaults during SDK initialization. Defaults stay on the device and are never sent to PNLight. The SDK uses them before the first successful fetch, when offline, or when a remote value is missing or has a different type. ```swift Swift theme={null} import PNLightSDK let config = PNLightConfig(remoteConfigDefaults: [ "paywall_enabled": .boolean(false), "welcome_title": .string("Welcome"), "trial_days": .number(7), "enabled_plans": .stringArray(["monthly"]), "paywall_style": .jsonObject([ "accent": .string("#6750A4"), "compact": .boolean(false), ]), ]) await PNLightSDK.shared.initialize(apiKey: "pnlight_sdk_token", config: config) ``` ```tsx React Native theme={null} import { initialize } from "@pnlight/sdk-react-native"; await initialize("pnlight_sdk_token", undefined, { paywall_enabled: false, welcome_title: "Welcome", trial_days: 7, enabled_plans: ["monthly"], paywall_style: { accent: "#6750A4", compact: false, }, }); ``` ```dart Flutter theme={null} import 'package:pnlight_sdk/pnlight_sdk.dart'; await PNLightSDK.initialize( 'pnlight_sdk_token', remoteConfigDefaults: { 'paywall_enabled': false, 'welcome_title': 'Welcome', 'trial_days': 7, 'enabled_plans': ['monthly'], 'paywall_style': { 'accent': '#6750A4', 'compact': false, }, }, ); ``` ## Fetch and activate Call `fetchAndActivate` after initialization when your app is ready to refresh Remote Config. By default, the SDK: * Waits briefly for AppsFlyer attribution so PNLight can apply attribution-based overrides. * Throttles successful fetches for 15 minutes. * Persists the active configuration per SDK token and PNLight user. * Keeps the previously active configuration if a fetch fails. Use an immediate fetch only for development, QA, or a startup path that must not wait for attribution. ```swift Swift theme={null} let result = await PNLightSDK.shared.fetchAndActivate() switch result { case .activated, .notModified: break case .throttled: break case .failed: break } let immediateResult = await PNLightSDK.shared.fetchAndActivate( minimumFetchInterval: 0, waitAttribution: false ) ``` ```tsx React Native theme={null} import { fetchAndActivate } from "@pnlight/sdk-react-native"; try { const result = await fetchAndActivate(); // "activated", "notModified", or "throttled" console.log("Remote Config fetch:", result); } catch (error) { // Previously active values and local defaults remain available. console.warn("Remote Config unavailable", error); } await fetchAndActivate(0, false); ``` ```dart Flutter theme={null} final result = await PNLightSDK.fetchAndActivate(); switch (result) { case RemoteConfigFetchResult.activated: case RemoteConfigFetchResult.notModified: break; case RemoteConfigFetchResult.throttled: break; case RemoteConfigFetchResult.failed: break; } await PNLightSDK.fetchAndActivate( minimumFetchInterval: Duration.zero, waitAttribution: false, ); ``` ## Read typed values Use typed getters with fallbacks. If a key is missing or the active value has a different type, the SDK returns the fallback. ```swift Swift theme={null} let paywallEnabled = PNLightSDK.shared.remoteConfigBoolean( forKey: "paywall_enabled", fallback: false ) let title = PNLightSDK.shared.remoteConfigString( forKey: "welcome_title", fallback: "Welcome" ) let trialDays = PNLightSDK.shared.remoteConfigNumber( forKey: "trial_days", fallback: 7 ) let plans = PNLightSDK.shared.remoteConfigStringArray( forKey: "enabled_plans", fallback: ["monthly"] ) let style = PNLightSDK.shared.remoteConfigJSONObject( forKey: "paywall_style", fallback: ["accent": .string("#6750A4")] ) ``` ```tsx React Native theme={null} import { getRemoteConfigBoolean, getRemoteConfigJSONObject, getRemoteConfigNumber, getRemoteConfigString, getRemoteConfigStringArray, } from "@pnlight/sdk-react-native"; const paywallEnabled = await getRemoteConfigBoolean("paywall_enabled", false); const title = await getRemoteConfigString("welcome_title", "Welcome"); const trialDays = await getRemoteConfigNumber("trial_days", 7); const plans = await getRemoteConfigStringArray("enabled_plans", ["monthly"]); const style = await getRemoteConfigJSONObject("paywall_style", { accent: "#6750A4", }); ``` ```dart Flutter theme={null} final paywallEnabled = await PNLightSDK.remoteConfigBoolean( 'paywall_enabled', fallback: false, ); final title = await PNLightSDK.remoteConfigString( 'welcome_title', fallback: 'Welcome', ); 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'}, ); ``` ## Recommended use * Keep app-critical defaults in the app so first launch and offline sessions work. * Read Remote Config through typed getters instead of casting raw JSON. * Use `minimumFetchInterval: 0` only in development or QA. * Fetch after forwarding attribution when campaign-specific overrides matter. * Coordinate key names and value types between dashboard owners and app developers before publishing. # Remote UI Source: https://docs.pnlight.app/sdk/remote-ui Render PNLight Remote UI placements such as paywalls and onboarding screens in your iOS, React Native, or Flutter app and handle user actions. PNLight Remote UI lets your app render screens you configure in the dashboard. Use it for paywalls, prompts, upgrade screens, and other app surfaces you may want to update without a new app release. ## How Remote UI is selected Your app requests a placement with a locale and user ID. PNLight returns the matching dashboard config when the placement is enabled and a usable config is available. Create every placement that your app can request in **Remote UI**. On a fresh fetch from PNLight (for example, when you pass `ignoreCache`), if the placement is missing, disabled, deleted, or missing a usable locale config, PNLight returns no UI for that request. The dashboard tracks: * Requests: users who requested the placement. * Shown: users who received a UI config that could be shown. PNLight can also apply targeting, show-once rules, capture protection, and runtime placeholder resolution before it returns a config. See [Remote UI placements](/dashboard/remote-ui-placements) for the dashboard controls. Runtime placeholders are resolved on the server. The SDK receives the final config values, such as product IDs, after PNLight replaces tokens like `{{monthlyProductId}}`. ## Render a placement `RemoteUiView` calls `getUIConfig` internally. On the first app launch, when you use attribution from AppsFlyer, wait 4-5 seconds after AppsFlyer initialization before showing the placement so conversion data has time to arrive. When `attributionRequired` is `true`, the SDK also waits for attribution internally before requesting the config. ```swift SwiftUI theme={null} RemoteUiView( placement: "paywall", cardId: "paywall_card", onPurchased: { productId in Task { await refreshEntitlements(for: productId) showMainScreen() } }, onAction: { action in if action.logId == "close_button" { closePaywall() } } ) ``` ```tsx React Native theme={null} { refreshEntitlements(productId).then(() => { navigation.navigate("Main"); }); }} onAction={(event) => { if (event.logId === "close_button") { navigation.goBack(); } }} /> ``` ```dart Flutter theme={null} RemoteUiView( placement: 'paywall', cardId: 'paywall_card', onPurchased: (productId) async { await refreshEntitlements(productId); openMainScreen(); }, onAction: (event) async { if (event.logId == 'close_button') { closePaywall(); } }, ) ``` ## Server-driven flows Remote UI can return a flow instead of a single card. A flow uses `schemaVersion: 2`, `type: "flow"`, an `initial_route`, and a `routes` object where each route contains a complete UI document. Render a flow with the same `RemoteUiView` integration. The SDK handles native navigation and modal presentation inside the Remote UI surface, so React Native and Flutter apps do not need extra navigation setup for PNLight routes. Use PNLight navigation URLs inside flow routes: | URL | Behavior | | -------------------------------------------- | ------------------------------------------------ | | `pnlight://navigation/push?route=details` | Pushes a route onto the active PNLight stack. | | `pnlight://navigation/pop` | Pops the active PNLight stack. | | `pnlight://navigation/replace?route=details` | Replaces the active PNLight route. | | `pnlight://navigation/pop_to_root` | Returns to the first PNLight route. | | `pnlight://navigation/present?route=offer` | Presents a PNLight route as a native modal flow. | | `pnlight://navigation/dismiss` | Dismisses the current PNLight modal flow. | PNLight consumes these navigation URLs inside a valid flow. Use app-specific URLs, action IDs, or action paths when the host app should handle an action through `onAction`. ## Remote UI components Use [Remote UI components](/sdk/remote-ui-components) to add PNLight components to a config and review their properties. ## Remote UI schema version PNLight versions the Remote UI envelope separately from SDK package versions. Put the version at the document root. * Schema v2 adds flows, safe areas, scaling variables, haptics, and dialogs. * Schema v3 keeps that behavior and adds `pnlight.purchase`, `pnlight.progress_bar`, and `pnlight.animated_number`. * Schema v4 keeps schema v3 behavior and adds `on_fail` and `on_cancel` follow-ups on `pnlight.purchase`. Existing schema v1 and v2 configs remain supported. They do not run `pnlight.purchase` or render schema v3 components. Schema v3 configs still run `on_success` only. ## Handle actions Remote UI actions are delivered to your app. Use action IDs, paths, or query parameters to decide what it should do. Different placements can return different actions, so make the handler explicit. Common actions: * Start a purchase with `pnlight.purchase`, or route to a host-app purchase flow when you need custom behavior. * Return the user to the main app screen after a successful subscription. * Close the screen. * Open another app screen. * Track a product interaction. Always handle unknown actions. A safe fallback is to close the Remote UI screen or ignore the action. ### SDK-owned purchase action Use `pnlight.purchase` when a Remote UI button should start a StoreKit purchase through the PNLight SDK. The document must declare `"schemaVersion": 3` or `"schemaVersion": 4`. ```json theme={null} { "log_id": "purchase_button", "typed": { "type": "custom" }, "payload": { "id": "pnlight.purchase", "params": { "product_id": "{{monthlyProductId}}", "on_success": [ { "log_id": "purchase_success", "url": "pnlight://navigation/replace?route=main" } ], "on_fail": [ { "log_id": "purchase_failed", "url": "pnlight://dialog/show?id=purchase_error" } ], "on_cancel": [ { "log_id": "purchase_cancelled", "typed": { "type": "set_variable", "variable_name": "is_purchasing", "value": { "type": "boolean", "value": false } } } ] } } } ``` `params.product_id` is required. Use a literal App Store product ID or a product runtime placeholder from the placement. These follow-up lists are optional. Each one accepts ordinary DivKit action dictionaries: * `on_success` runs after a successful purchase. Schema v3 and v4 both support it. * `on_fail` runs when the purchase throws an error. This needs schema v4. * `on_cancel` runs when the user dismisses the StoreKit sheet. This also needs schema v4. A pending StoreKit purchase runs no follow-up. The current Remote UI stays in place, because Apple may still approve the transaction later. Repeated taps are ignored while one SDK-owned purchase is already in progress. Schema v3 configs keep the original success-only behavior. Cancellation or an error leaves the current Remote UI in place. In the dashboard preview, `pnlight.purchase` opens a simulator. **Success** runs `on_success`. In schema v4, **Fail** runs `on_fail`, and **Cancel** or dismissing the simulator runs `on_cancel`. Schema v3 preview still has **Success** and **Fail** only. **Fail** just closes the simulator. No real StoreKit transaction occurs in the preview. Use the `onPurchased` callback on `RemoteUiView` when the app needs to refresh entitlements, unlock content, or update navigation after a Remote UI purchase. The callback receives the purchased product ID. It is emitted only for purchases started by that Remote UI view, not for manual SDK purchase calls. ## Fetch configs manually Use `getUIConfig` if you need to fetch the placement configuration yourself. On the first app launch, when attribution is required, call it after sending attribution and wait 4-5 seconds after AppsFlyer initialization before requesting the config. The SDK still waits for attribution internally when `attributionRequired` is `true`. ```swift Swift theme={null} try? await Task.sleep(nanoseconds: 5_000_000_000) let config = await PNLightSDK.shared.getUIConfig(placement: "paywall") let freshConfig = await PNLightSDK.shared.getUIConfig( placement: "paywall", ignoreCache: true ) ``` ```tsx React Native theme={null} import { getUIConfig } from "@pnlight/sdk-react-native"; await new Promise((resolve) => setTimeout(resolve, 5000)); const config = await getUIConfig("paywall"); const freshConfig = await getUIConfig("paywall", true, true); ``` ```dart Flutter theme={null} await Future.delayed(const Duration(seconds: 5)); final config = await PNLightSDK.getUIConfig('paywall'); final freshConfig = await PNLightSDK.getUIConfig( 'paywall', ignoreCache: true, ); ``` ## Config cache behavior Remote UI configs are cache-first by default. If a cached config exists, the SDK can return it immediately and refresh it in the background with an ETag request. This keeps repeat displays responsive while still picking up dashboard changes. Use `ignoreCache` when the app needs the latest server response, such as during QA or after a user changes a state that should affect targeting. ## Handle missing configs and errors On a fresh fetch from PNLight, a `null` or empty config means PNLight did not return UI for that placement. This can happen when the placement does not exist in **Remote UI**, the placement is disabled, targeting does not match, show-once rules suppress the placement, capture protection blocks the user, no matching locale config exists, or a runtime placeholder cannot be resolved. React Native and Flutter wrappers surface loading errors separately from missing UI. Use `onError` on `RemoteUiView` or handle `getUIConfig` errors when you fetch manually. Swift apps that fetch manually can use `getUIConfigResult` to distinguish all outcomes: ```swift theme={null} let result = await PNLightSDK.shared.getUIConfigResult(placement: "paywall") switch result { case .success(let config?): render(config) case .success(nil): showLocalFallback() case .failure: showErrorFallback() } ``` ## Dashboard setup Create and manage placements in **Remote UI**. See [Remote UI placements](/dashboard/remote-ui-placements) for the dashboard workflow. # Remote UI components Source: https://docs.pnlight.app/sdk/remote-ui-components Add PNLight components to a Remote UI configuration and configure their supported properties. Use Remote UI components when the standard configuration does not provide the behavior you need. Add a component with `"type": "custom"` and its `custom_type` value. This page covers the PNLight components you can add to a Remote UI configuration. Components marked as schema v3 require `"schemaVersion": 3` in the Remote UI document. Components marked as schema v3 require `"schemaVersion": 3` at the Remote UI document root. ## Circular loader `pnlight.circular_loader` displays a circular loading indicator in your Remote UI screen. Use it while your app or the screen loads content. ```json theme={null} { "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" } } ``` | Property | Use | | --------------------- | --------------------------------------------------------------------------------- | | `style` | Set `medium` or `large`. The default is `medium`. | | `color` | Set a `#RRGGBB` or `#AARRGGBB` color. The default follows the system label color. | | `accessibility_label` | Set the label announced by assistive technology. The default is `Loading`. | Set the component size with its `width` and `height` fields. Use `{ "type": "wrap_content" }` for either dimension when you want the loader to use its intrinsic size. ## Progress bar `pnlight.progress_bar` displays an animated linear progress bar. Use it for scan progress, onboarding completion, loading steps, or any Remote UI state where a filled bar should move smoothly between values. ```json theme={null} { "type": "custom", "custom_type": "pnlight.progress_bar", "width": { "type": "match_parent" }, "height": { "type": "fixed", "value": 10 }, "custom_props": { "instance_id": "scan_progress_bar", "progress": 0, "progress_variable": "scan_progress", "animation_duration": 0.45, "track_color": "#FFE5E5EA", "fill_color": "#FF007AFF" } } ``` | Property | Use | | -------------------------- | ---------------------------------------------------------------------------------------- | | `instance_id` | Set a stable ID for this bar. Use a unique value for each bar rendered at the same time. | | `progress` | Set the target fill from `0` to `1`. Values outside this range are clamped. | | `progress_variable` | Set a DivKit variable name that updates `progress` reactively. | | `initial_progress` | Set the first rendered fill when you want the bar to animate in on appear. | | `indeterminate` | Set `true` for a looping sliding band when progress is unknown. | | `track_color` | Set the track color as `#RRGGBB` or `#AARRGGBB`. | | `track_height` | Set the intrinsic thickness used when `height` is `wrap_content`. | | `fill_color` | Set the fill color or the start color for a gradient. | | `fill_color_end` | Set a second fill color to render a horizontal gradient. | | `corner_radius` | Set the track and fill corner radius. The default is a pill shape. | | `fill_inset` | Add spacing between the track bounds and the fill. | | `animation_duration` | Set the seconds spent moving to a new value. | | `indeterminate_duration` | Set the seconds for one indeterminate band pass. | | `indeterminate_band_width` | Set the indeterminate band width as a fraction of the track width. | | `reduced_motion` | Set `true` to apply values immediately without animation. | | `accessibility_label` | Set the label announced by assistive technology. The value is announced as a percentage. | This is a schema v3 component. The Remote UI document must declare `"schemaVersion": 3`. ## Animated number `pnlight.animated_number` displays a number that counts between values. Use it for counters, savings amounts, progress summaries, or metrics that should update smoothly. ```json theme={null} { "type": "custom", "custom_type": "pnlight.animated_number", "width": { "type": "match_parent" }, "height": { "type": "wrap_content" }, "custom_props": { "instance_id": "issues_count", "value": 0, "value_variable": "issues_found", "suffix": " issues found", "font_size": 28, "font_weight": "bold" } } ``` | Property | Use | | --------------------- | ---------------------------------------------------------------------------------------------------- | | `instance_id` | Set a stable ID for this number. Use a unique value for each number rendered at the same time. | | `value` | Set the target number. | | `value_variable` | Set a DivKit variable name that updates `value` reactively. | | `initial_value` | Set the first rendered value when you want the number to count in on appear. | | `decimals` | Set the number of fraction digits. The same value is used for minimum and maximum fraction digits. | | `min_integer_digits` | Pad shorter numbers with leading zeroes. | | `grouping` | Enable or disable thousands separators. The default is enabled. | | `monospaced_digits` | Use tabular figures so the label does not jitter while counting. | | `prefix` | Add text before the formatted number. | | `suffix` | Add text after the formatted number. | | `font_size` | Set the text size in points. | | `font_weight` | Set `ultralight`, `thin`, `light`, `regular`, `medium`, `semibold`, `bold`, `heavy`, or `black`. | | `text_color` | Set the text color as `#RRGGBB` or `#AARRGGBB`, or use the default adaptive text color. | | `text_alignment` | Set `left`, `center`, `right`, or `natural`. | | `animation_duration` | Set the seconds spent counting to a new value. | | `curve` | Set `linear`, `ease_in`, `ease_out`, or `ease_in_out`. | | `reduced_motion` | Set `true` to apply values immediately without counting. | | `accessibility_label` | Set the label announced by assistive technology. The target value is announced as the element value. | This is a schema v3 component. The Remote UI document must declare `"schemaVersion": 3`. Preview the configuration in **Remote UI**, then test the placement in your app before you enable it for users. ## Progress bar (schema v3) `pnlight.progress_bar` displays an animated progress indicator. Use it in schema v3 documents when a screen needs to show completion, scanning, loading, or another fractional state. ```json theme={null} { "type": "custom", "custom_type": "pnlight.progress_bar", "width": { "type": "match_parent" }, "height": { "type": "fixed", "value": 10 }, "custom_props": { "instance_id": "scan_progress", "progress": 0.72, "initial_progress": 0, "fill_color": "#FF087EF5", "fill_color_end": "#FF34C759", "animation_duration": 1.4 } } ``` | Property | Use | | -------------------------- | --------------------------------------------------------------------------- | | `instance_id` | Keeps the displayed progress stable if the Remote UI view re-renders. | | `progress` | Sets the target progress from `0` to `1`. | | `progress_variable` | Subscribes the component to a DivKit variable for progress updates. | | `initial_progress` | Sets the first displayed value before the component animates to `progress`. | | `indeterminate` | Shows a moving band instead of a fixed progress value. | | `track_color` | Sets the track color. | | `track_height` | Sets the minimum track height when the DivKit height is `wrap_content`. | | `fill_color` | Sets the fill color. | | `fill_color_end` | Adds a second color for a horizontal gradient fill. | | `corner_radius` | Sets the track and fill corner radius. | | `fill_inset` | Adds padding between the track and the fill. | | `animation_duration` | Sets the progress animation duration in seconds. | | `indeterminate_duration` | Sets the indeterminate animation duration in seconds. | | `indeterminate_band_width` | Sets the width of the moving band as a fraction of the track. | | `reduced_motion` | Disables the animation when set to `true`. | | `accessibility_label` | Sets the accessibility label. The default is `Progress`. | For variable-driven progress, keep `progress` as the literal initial value and put the DivKit variable name in `progress_variable`. ## Animated number (schema v3) `pnlight.animated_number` displays a number that counts from its current value to a new target. Use it in schema v3 documents for counters, scan results, scores, and other numeric summaries. ```json theme={null} { "type": "custom", "custom_type": "pnlight.animated_number", "width": { "type": "match_parent" }, "height": { "type": "wrap_content" }, "custom_props": { "instance_id": "items_scanned", "value": 1284, "initial_value": 0, "suffix": " items scanned", "font_size": 28, "font_weight": "bold", "text_alignment": "left", "animation_duration": 1.4 } } ``` | Property | Use | | --------------------- | ------------------------------------------------------------------------ | | `instance_id` | Keeps the displayed number stable if the Remote UI view re-renders. | | `value` | Sets the target number. | | `value_variable` | Subscribes the component to a DivKit variable for value updates. | | `initial_value` | Sets the first displayed value before the component animates to `value`. | | `font_size` | Sets the text size. | | `font_weight` | Sets the text weight. | | `text_color` | Sets the text color. | | `text_alignment` | Sets `left`, `center`, `right`, or `natural` alignment. | | `decimals` | Sets the number of decimal places. | | `min_integer_digits` | Pads the integer part with leading zeroes. | | `grouping` | Turns locale-aware digit grouping on or off. | | `monospaced_digits` | Uses tabular digits to avoid visual jitter while counting. | | `prefix` | Adds text before the number. | | `suffix` | Adds text after the number. | | `animation_duration` | Sets the count animation duration in seconds. | | `curve` | Sets `linear`, `ease_in`, `ease_out`, or `ease_in_out`. | | `reduced_motion` | Disables the animation when set to `true`. | | `accessibility_label` | Sets the accessibility label. | For variable-driven counts, keep `value` as the literal initial value and put the DivKit variable name in `value_variable`. # Core concepts Source: https://docs.pnlight.app/start/core-concepts Learn the PNLight concepts you use during integration, including projects, SDK tokens, users, events, attribution sources, Remote Config, and Remote UI placements. You will use the same core concepts in the SDK and in the dashboard. ## Project A project represents one app in PNLight. It keeps the app identifiers, SDK token, App Store settings, product IDs, attribution settings, Remote Config, Remote UI placements, team access, and analytics together. The dashboard home page is **My apps**. It lists every app you can access. ## SDK token The SDK token identifies the PNLight project from your app. Copy it from [Integration](/dashboard/sdk-status) and pass it to the SDK during initialization. Treat the SDK token as an app configuration value. Do not publish it in public examples, screenshots, or issue reports. ## User ID PNLight creates or returns a stable user ID through the SDK. Use this ID when you send events, attribution, and Remote UI requests. ## Attribution Attribution links a user to a source such as AppsFlyer or internal click attribution. PNLight uses ad networks and campaigns to group paid traffic in the dashboard. You can choose the priority order in **Settings** and set how long PNLight should wait for a preferred attribution source before falling back. ## Remote Config Remote Config lets your app fetch typed settings from the dashboard. The base configuration defines each key and its value type. Targeted overrides replace values for matching app installations. ## Remote UI Remote UI lets your app render screens configured in the dashboard. A UI request uses a placement ID, locale, and user ID. Common placements include paywalls, onboarding prompts, and upgrade screens. ## Placement A placement is a named location where your app can show Remote UI. Use stable placement IDs such as `paywall`, `onboarding_offer`, or `upgrade_prompt`. ## Analytics Analytics pages help you follow users, app events, and purchases through your app. Review clicks, installs, revenue, spend, trials, subscriptions, ARPPU, rebills, App Events, Remote UI, and funnels in the dashboard. Open **Customers** and **Events** when you need to investigate one user or a raw event. # Quickstart Source: https://docs.pnlight.app/start/quickstart Set up a PNLight project, install the SDK in your iOS, React Native, or Flutter app, and confirm installs, attribution, Remote Config, and Remote UI requests are working. Use this guide to turn a new project into a working SDK integration. ## Prerequisites * Access to the PNLight dashboard. * Your app bundle ID. * Your App Store ID. * An app build that can install a native iOS dependency. Open the [**dashboard**](https://console.pnlight.app/dashboard/) and click **New App**. Add your app information, choose the preferred attribution source, and optionally connect **App Store Connect**. Work through the **Getting started** checklist in the dashboard sidebar. PNLight updates **Set up App Store notifications**, **Install PNLight SDK**, and **Send an in-app event** when it receives the corresponding signals. Add the SDK by following the [Swift](/sdk/ios-swift), [React Native](/sdk/react-native), or [Flutter](/sdk/flutter) guide. Copy the project token from [**Integration**](/dashboard/sdk-status), then pass it to the SDK initialization method at app startup before you send attribution, fetch Remote Config, or request Remote UI. If you use AppsFlyer or another attribution provider, send attribution data through the SDK after the provider returns conversion data. If your app uses Remote Config, publish typed values in the dashboard, then call `fetchAndActivate` and read values with typed getters. Request Remote UI where it fits your app experience, then confirm that the published interface appears in the app. Open **Integration**, **Analytics**, and **Remote UI** to confirm installs, attribution, app events, and UI requests are arriving. Your initial PNLight integration is ready. Next, configure the platform around your product goals. Use one PNLight project per app and environment that needs separate reporting. Do not reuse a production SDK token in development builds unless you want test events in production analytics. ## Minimal SDK initialization ```swift Swift theme={null} import PNLightSDK await PNLightSDK.shared.initialize(apiKey: "pnlight_sdk_token") ``` ```tsx React Native theme={null} import { initialize } from "@pnlight/sdk-react-native"; await initialize("pnlight_sdk_token"); ``` ```dart Flutter theme={null} import 'package:pnlight_sdk/pnlight_sdk.dart'; await PNLightSDK.initialize('pnlight_sdk_token'); ``` ## Next steps * Fetch typed runtime settings with [Remote Config](/sdk/remote-config). * Render server-configured screens with [Remote UI](/sdk/remote-ui). * Review app performance with [Analytics](/dashboard/analytics). * Configure your app's [products](/dashboard/products). * Connect [ad networks](/dashboard/ad-networks) and set up [campaigns](/dashboard/campaigns). * Invite [team members](/dashboard/team). # Release checklist Source: https://docs.pnlight.app/start/release-checklist Verify the PNLight SDK setup, App Store integration, Remote Config, Remote UI placements, ad attribution setup, and analytics events before shipping a new app build. Use this checklist before you submit a build or send production traffic to PNLight. ## SDK * The app initializes the SDK with the production project SDK token. * Development and staging builds do not send data to the production project unless intended. * The app can read the PNLight user ID. * Attribution data is sent after the attribution provider returns it. ## App Store * **Integration → AppStore** contains the correct issuer ID and shared secret. * The `.p8` in-app purchase key is uploaded without renaming the file. * App Store Server Notifications use the project notification URL shown in **Integration → AppStore**. * The notification URL is saved in both production and sandbox fields in App Store Connect. * App Store Server Notifications use version 2. ## Remote UI * Each placement requested by the app exists in **Remote UI** and has a stable ID. * Required locales have valid JSON configs. * The placement is enabled. * Test users can render the screen in the app. * Button actions are handled by app code. ## Remote Config * Each key read by the app exists in **Remote Config** with the expected type. * The app provides local fallbacks for Remote Config values needed at startup or offline. * The current draft is published before release. * Targeted overrides match the intended campaigns, devices, countries, or SDK versions. ## Dashboard * Installs appear in **Analytics**. * Purchases appear on customer profiles. * Ad networks and campaigns are configured if you use paid acquisition. * Test users are marked correctly if you rely on debug-only Remote UI or QA traffic. * Team members have the right access. Keep this checklist with your release process. Problems found after release often come from the wrong project token, missing App Store settings, or a disabled Remote UI placement. # App Store integration issues Source: https://docs.pnlight.app/troubleshooting/app-store-integration Fix App Store integration issues in PNLight, including invalid issuer ID or shared secret, missing in-app purchase key, and App Store notification errors. Use this page when App Store purchases, renewals, or notifications do not appear in PNLight. ## Credential checklist * Bundle ID matches the iOS app. * App Store ID matches the App Store listing. * Issuer ID is copied from App Store Connect. * Shared secret is current. * The `.p8` key is uploaded without renaming. * The App Store notification URL is copied from **Integration → AppStore** for the same PNLight project. ## Purchase succeeds but does not appear in PNLight When StoreKit completes a purchase and the transaction is verified, the PNLight SDK returns a successful purchase result to your app. Do not block the customer after a successful StoreKit purchase. Their purchase is already complete. If the SDK cannot report the App Store receipt to PNLight right away, it tries again later. PNLight can also use the saved receipt and App Store credentials to recover the original transaction and subscription history when the transaction is not immediately available. If the purchase does not appear in PNLight after a short delay: 1. Use the credential checklist above. 2. Confirm the shared secret and `.p8` in-app purchase key are saved for the same App Store app. 3. Open the app again with the same PNLight user ID so the SDK can send the latest receipt. 4. Test with a fresh sandbox transaction after fixing credentials or notification settings. ## Notifications do not arrive Check App Store Connect: 1. Open the app in App Store Connect. 2. Open server notification settings. 3. Confirm the notification URL matches the URL shown in PNLight **Integration → AppStore**. 4. Save the App Store Connect settings. 5. Make a fresh sandbox purchase or renewal test. If you recently changed credentials or notification URLs, test with a new transaction. Old transactions might not replay automatically. # Attribution issues Source: https://docs.pnlight.app/troubleshooting/attribution Troubleshoot missing or delayed attribution in PNLight by checking SDK calls, provider names, attribution priority, wait windows, and dashboard settings. Use this page when a user or Remote UI request is not attributed as expected. ## Check the app integration * The app sends attribution after the attribution provider returns data. * The provider name matches the SDK-supported value. * The identifier is stable for the provider. * The same PNLight user ID is used for SDK actions around install, attribution, and Remote UI. * If PNLight initialized before ATT authorization, the app calls `updateIdfa()` after the prompt completes. ## Check dashboard settings Open **Settings** and review: * Attribution priority order. * Enabled attribution sources. * Wait window for preferred attribution. ## Attribution is delayed Attribution can take longer while PNLight waits for the preferred provider before falling back. If that provider reports no attribution, PNLight can continue to the next enabled source before the full wait window ends. Review the wait window with your product and marketing teams when an early app flow depends on attribution. On the first app launch, wait 4-5 seconds after AppsFlyer initialization before requesting Remote UI when attribution is required. The SDK also waits for attribution internally when `attributionRequired` is `true`. For launch testing, use one test device and one attribution source at a time. This makes it easier to confirm which provider supplied the final attribution. # Remote UI issues Source: https://docs.pnlight.app/troubleshooting/remote-ui Resolve PNLight Remote UI problems such as missing placements, broken action handlers, locale fallbacks, and incorrect display counts on iOS apps. Use this page when a Remote UI placement does not render or actions do not work. ## Placement does not render Check: * The app requests the same placement ID that exists in **Remote UI**. * The placement is enabled. * The requested locale has a config, or the placement has a default locale. * The config JSON is valid. * Runtime placeholders have matching definitions. * Product placeholders point to existing products. * The user has not already seen a placement configured as **Show only once per user**. * The user matches the placement targeting conditions. * Capture protection has not blocked this user. ## Requests increase but shown does not This usually means PNLight received the request but had no config it could return for display. Common causes: * Placement missing from **Remote UI**. * Placement disabled. * Missing locale config. * Missing or unresolved runtime placeholder. * Attribution is required but not resolved yet. * Show-once rules suppress the placement for that user. * Targeting conditions do not match the user. * Capture protection blocks the user after capture is detected. ## Placeholder does not resolve Check the placement in **Remote UI**: * Every placeholder token uses the `{{key}}` format. * Each token has a matching placeholder definition. * Placeholder keys start with a letter and contain only letters, numbers, and underscores. * Product placeholders point to products that still exist in [Products](/dashboard/products). If PNLight cannot resolve a placeholder, it does not return that placement config. ## Config request fails Recent SDK wrappers separate loading errors from "no UI for this placement". * In Swift, use `getUIConfigResult` when you fetch configs manually. * In React Native and Flutter, handle `onError` on `RemoteUiView`. * During QA, pass `ignoreCache` when you need a fresh server response. ## Actions do not work Check your app action handler: * Handle the expected `logId`, path, or action value. * Read parameters defensively. * Provide a fallback for unknown actions. ## Purchase follow-ups do not run Check the Remote UI document: * `on_success` needs `"schemaVersion": 3` or `"schemaVersion": 4`. * `on_fail` and `on_cancel` need `"schemaVersion": 4`. Schema v3 configs still run `on_success` only. * A pending StoreKit purchase runs no follow-up. The current screen stays in place until Apple approves or fails the transaction. ## Flow navigation does not work For server-driven flows, check: * The config uses `schemaVersion: 2` and `type: "flow"`. * The destination route exists in the `routes` object. * The navigation URL is inside a valid flow route. * The app uses the current Swift SDK, React Native wrapper, or Flutter wrapper for its integration. PNLight handles `pnlight://navigation/...` URLs inside flows before they reach `onAction`. Use app-specific URLs, action IDs, or action paths for actions the host app should handle. Use debug mode or a test device while testing display rules. Turn debug mode off before production traffic unless your team intentionally needs it. # SDK setup issues Source: https://docs.pnlight.app/troubleshooting/sdk-setup Fix common PNLight SDK installation and initialization problems on iOS, React Native, and Flutter, including missing token, init order, and dependency errors. Use this page when the app builds but PNLight calls fail, or when you cannot find data in the dashboard. ## Check initialization * The app calls SDK initialization once during startup. * The SDK token comes from the same project you are checking in the dashboard. * The initialization call finishes before attribution or Remote UI requests. * Unsupported platforms do not call iOS-only SDK code. ## Check package installation Confirm the Swift package URL is `https://github.com/pnlight-dev/sdk-swift.git` and the app target includes the package. Reinstall pods and rebuild the native app after adding `@pnlight/sdk-react-native`. Run `flutter pub get`, install pods, and rebuild the iOS app after adding `pnlight_sdk`. ## No data in dashboard If data does not appear: 1. Open the [Integration](/dashboard/sdk-status) page to see which signals have been received. 2. Confirm the app build uses the correct SDK token. 3. Trigger a fresh install or test event. 4. Check the selected date range in **Analytics**. 5. Check whether you are viewing the correct project. A common setup mistake is using a staging token in a production build or a production token in a development build.