> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pnlight.app/llms.txt
> Use this file to discover all available pages before exploring further.

# 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 configured in the dashboard. Use it for paywalls, prompts, upgrade screens, and other app surfaces that you want to update without a new app release.

## How Remote UI is selected

The app requests a placement with a locale and user ID. PNLight returns a config from the matching dashboard placement when the placement is enabled and a matching 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.

<CodeGroup>
  ```swift SwiftUI theme={null}
  RemoteUiView(placement: "paywall", cardId: "paywall_card") { action in
      if action.logId == "purchase_button" {
          let productId = action.params["id"] ?? ""
          Task {
              let success = await purchase(productId)
              if success {
                  showMainScreen()
              }
          }
      }
  }
  ```

  ```tsx React Native theme={null}
  <RemoteUiView
    placement="paywall"
    cardId="paywall_card"
    style={{ flex: 1 }}
    onAction={(event) => {
      if (event.logId === "purchase_button") {
        startPurchase(event.params.id).then(() => {
          navigation.navigate("Main");
        });
      }
    }}
  />
  ```

  ```dart Flutter theme={null}
  RemoteUiView(
    placement: 'paywall',
    cardId: 'paywall_card',
    onAction: (event) async {
      if (event.path == 'purchase') {
        final success = await startPurchase(event.params['id']);
        if (success) {
          openMainScreen();
        }
      }
    },
  )
  ```
</CodeGroup>

## Remote UI components

Use [Remote UI components](/sdk/remote-ui-components) to add PNLight components to a config and review their properties.

## Handle actions

Remote UI actions are delivered to your app. Use action IDs, paths, or query parameters to decide what the app should do. Different placements can return different actions, so keep the handler explicit.

Common actions:

* Start a purchase.
* Return the user to the main app screen after a successful subscription.
* Close the screen.
* Open another app screen.
* Track a product interaction.

<Warning>
  Always handle unknown actions. A safe fallback is to close the Remote UI screen or ignore the action.
</Warning>

## 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`.

<CodeGroup>
  ```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<void>.delayed(const Duration(seconds: 5));

  final config = await PNLightSDK.getUIConfig('paywall');
  final freshConfig = await PNLightSDK.getUIConfig(
    'paywall',
    ignoreCache: true,
  );
  ```
</CodeGroup>

## 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 must wait for the latest server response, such as during QA or after the 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.
