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

# React Native Screens

> Observe server-driven screens designed in the AppDNA Console, rendered natively

<Info>
  **Supported on:** iOS SDK `1.0.70+` · Android SDK `1.0.42+` · React Native SDK `1.0.7+` (New Architecture only)
</Info>

## Overview

Server-driven screens let growth teams design and deploy native screens from the AppDNA Console without an app release. Screens are composed from the same content-block and section library used by paywalls, onboarding, in-app messages, and surveys, then delivered to the SDK via the remote config bundle. On React Native, screens are rendered fully natively -- SwiftUI on iOS and Jetpack Compose on Android -- so the experience matches a hand-built native UI.

Use screens to ship feature announcements, upgrade prompts, referral surfaces, guided tutorials, and other composable native surfaces from the Console.

## Show a Screen

```typescript theme={null}
import { AppDNA } from '@appdna-ai/react-native-sdk';

const presented = await AppDNA.screens.show('upgrade_prompt');
if (!presented) {
  console.log('Nothing could present the screen — no view controller / activity was available');
}
```

`show(...)` resolves a **boolean**: whether the screen was presented. It does **not** resolve the
outcome, and it cannot: a screen can be dismissed minutes after it appears. What the user did arrives
on `onScreenDismissed` on the delegate you register with `setDelegate(...)` — see
[Screen Delegate](#screen-delegate) below.

## Show a Multi-Screen Flow

```typescript theme={null}
const presented = await AppDNA.screens.showFlow('onboarding_v2');
if (!presented) {
  console.log('Nothing could present the flow');
}
```

`showFlow(...)` likewise resolves whether the flow was presented. The result — completion, the screens
viewed, the responses — arrives on the delegate's `onFlowCompleted`.

## Dismiss

```typescript theme={null}
await AppDNA.screens.dismiss();
```

## `<AppDNAScreenSlot>` — inline screens

`show(...)` and `showFlow(...)` present a screen **over** your app. A slot embeds one **inside** your
own layout: you place `<AppDNAScreenSlot name="...">` in your React tree, the Console assigns a screen
to that slot name, and the native renderer draws it in place. It is the only React component the
package exports.

```tsx theme={null}
import { View, Text } from 'react-native';
import { AppDNAScreenSlot } from '@appdna-ai/react-native-sdk';

export function HomeScreen() {
  return (
    <View style={{ flex: 1 }}>
      <Text>Today</Text>

      <AppDNAScreenSlot
        name="home_promo"
        minHeight={120}
        style={{ marginVertical: 16 }}
        onContentSizeChange={({ height }) => console.log('slot height', height)}
      />
    </View>
  );
}
```

| Prop                  | Type                                                   | Description                                                                                                                                                                                   |
| --------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                | `string`                                               | **Required.** The Console slot name to render.                                                                                                                                                |
| `minHeight`           | `number?`                                              | Height reserved before the first measurement lands. Defaults to `0` — set it near the slot's expected height, or the layout visibly collapses on the first frame and expands one frame later. |
| `style`               | `StyleProp<ViewStyle>?`                                | Extra style for the slot container. `height` is managed internally — do not set it here.                                                                                                      |
| `onContentSizeChange` | `((size: { width: number; height: number }) => void)?` | Called with the measured content height (points / dp) whenever it changes.                                                                                                                    |

* An empty slot renders nothing — no visual impact and no reserved space once measured.
* Content updates on the next config refresh.
* Audience targeting applies per slot.
* The slot raises **no delegate callbacks**: `onScreenPresented` / `onScreenDismissed` /
  `onFlowCompleted` belong to `show(...)` / `showFlow(...)`. A slot is content, not a presentation.

<Warning>
  **iOS + `use_frameworks!`: one required step.** If your `Podfile` uses `use_frameworks!` (any app with
  Firebase in its pod graph ends up needing it, and the AppDNA core SDK depends on Firebase), React
  Native's generated component registry is compiled out — nothing registers the slot's native view.
  There is no error and no warning: `<AppDNAScreenSlot>` simply renders React's
  `Unimplemented component: <AppdnaScreenSlotView>` placeholder and your slot never appears.

  Add this override to `AppDelegate.mm` — `RCTAppDelegate` already exposes the hook:

  ```objc theme={null}
  #import <appdna_sdk_react_native/AppdnaFabricComponents.h>

  - (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
  {
    NSMutableDictionary *components = [[super thirdPartyFabricComponents] mutableCopy];
    [components addEntriesFromDictionary:AppdnaFabricComponents()];
    return components;
  }
  ```

  Only the inline slot needs this. Every method, event, and delegate works without it. Apps that link
  pods as static libraries need nothing.
</Warning>

## Navigation Interception

Inject screens automatically between app navigations:

```typescript theme={null}
// Intercept every navigation
await AppDNA.screens.enableNavigationInterception();

// Or scope to specific routes — a plain string array (exact match or trailing `*` wildcard)
await AppDNA.screens.enableNavigationInterception(['SettingsPage', 'Premium*']);

// To stop intercepting
await AppDNA.screens.disableNavigationInterception();
```

When interception is enabled, the SDK consults the registered `AppDNAScreenDelegate` on every navigation event so you can route or suppress as needed. Pass an array of route names to limit interception to an allowlist — exact names and trailing `*` wildcards.

## Debug Preview

Render a screen from raw JSON for design or QA preview without publishing to the Console:

`preview` takes the screen config as a **JSON string**, not an object:

```typescript theme={null}
await AppDNA.screens.preview(
  JSON.stringify({
    id: 'test',
    name: 'Test',
    presentation: 'modal',
    layout: { type: 'scroll' },
    sections: [],
  }),
);
```

It resolves a boolean — whether the preview was presented. Use during development only.

## Module Access

Access the screen module through the `AppDNA.screens` namespace:

```typescript theme={null}
const screen = AppDNA.screens;
```

| Method                          | Signature                                                            | Description                                                                                                                                |
| ------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `show`                          | `show(screenId: string): Promise<boolean>`                           | Present a server-driven screen by ID. Resolves whether it was presented; the outcome arrives on `onScreenDismissed`.                       |
| `showFlow`                      | `showFlow(flowId: string): Promise<boolean>`                         | Present a multi-screen flow. The result arrives on `onFlowCompleted`.                                                                      |
| `dismiss`                       | `dismiss(): Promise<void>`                                           | Dismiss the currently-presented screen                                                                                                     |
| `enableNavigationInterception`  | `enableNavigationInterception(screens?: string[]): Promise<void>`    | Inject screens between app navigations. Pass an allowlist of route names (exact match or trailing `*` wildcard); omit it to intercept all. |
| `disableNavigationInterception` | `disableNavigationInterception(): Promise<void>`                     | Stop intercepting navigation                                                                                                               |
| `preview`                       | `preview(json: string): Promise<boolean>`                            | Render a screen from a raw JSON **string** for debugging                                                                                   |
| `setDelegate`                   | `setDelegate(delegate: Partial<AppDNAScreenDelegate> \| null): void` | Observe screen lifecycle from your TypeScript app. Pass `null` to clear it — registering again replaces it.                                |

## Audience-Based Presentation

In addition to imperative `show(...)` calls, screens can be presented automatically by Console-driven audience targeting:

1. Design the screen in the AppDNA Console.
2. Attach a trigger (event-based, schedule-based, or app-state-based) and an audience.
3. Publish the screen.

When a user enters the audience, the SDK fetches the latest config bundle and presents the screen at the configured trigger point. The required wiring is minimal:

* Call `AppDNA.configure(...)` at app startup.
* Identify the user with `AppDNA.identify(...)` so targeting rules can resolve.
* Track relevant events with `AppDNA.track(...)` so event triggers can fire.

Lifecycle callbacks fire on the registered delegate either way.

## Screen Delegate

`AppDNAScreenDelegate` exposes the four screen lifecycle methods. The delegate is implemented as an object literal or class and registered with the module:

```typescript theme={null}
import { AppDNA, AppDNAScreenDelegate } from '@appdna-ai/react-native-sdk';

const screenObserver: AppDNAScreenDelegate = {
  onScreenPresented(screenId) {
    console.log(`Screen shown: ${screenId}`);
  },

  onScreenDismissed(screenId, result) {
    const dismissed = (result.dismissed as boolean) ?? false;
    const responses = result.responses as Record<string, unknown> | undefined;
    const lastAction = result.last_action as string | undefined;
    console.log(`Screen ${screenId} dismissed=${dismissed}, action=${lastAction}`);
  },

  onFlowCompleted(flowId, result) {
    const completed = (result.completed as boolean) ?? false;
    const screensViewed = (result.screens_viewed as string[]) ?? [];
    console.log(`Flow ${flowId} completed=${completed}, screens=`, screensViewed);
  },

  onScreenAction(screenId, action) {
    const type = action.type as string | undefined;
    console.log(`Screen ${screenId} action: ${type}`);
    // Return false to intercept the action and apply custom handling.
    return true;
  },
};

AppDNA.screens.setDelegate(screenObserver);
```

<Note>
  Pass `null` to clear a delegate. Registering a new delegate
  **replaces** the previous one, and `AppDNA.shutdown()` detaches everything. Register once at app
  startup rather than from a component that mounts and unmounts.
</Note>

### Delegate Method Reference

| Method              | Description                                                                                                                                                                    |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `onScreenPresented` | Fires when a screen appears, alongside the `screen_presented` analytics event.                                                                                                 |
| `onScreenDismissed` | Fires when a screen is dismissed. `result` carries dismissal context (e.g. `dismissed`, `responses`, `last_action`, `duration_ms`).                                            |
| `onFlowCompleted`   | Fires when a multi-screen flow ends. `result.completed` is `true` when the user reached the final screen, `false` when abandoned.                                              |
| `onScreenAction`    | Fires for every section action the user takes on a screen. Return `false` from your override to intercept the action; return `true` to let the SDK apply its default handling. |

Result and action payloads are delivered as `Record<string, unknown>` rather than typed classes. This keeps the bridge forward-compatible with new fields the native SDKs add (for example, additions like `last_action` or `duration_ms`) without requiring a React Native package bump.

## Sections and Content Blocks

Screens are composed of an ordered list of **sections**. Each section pulls from the unified registry shared across all SDK modules, so the same content blocks you use in paywalls, onboarding, in-app messages, and surveys are available inside screens:

| Category   | Section Types                                                                                           |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| Generic    | `content_blocks`, `hero`, `spacer`, `divider`, `cta_footer`, `sticky_footer`                            |
| Onboarding | `onboarding_step`, `progress_indicator`, `navigation_controls`                                          |
| Paywall    | `paywall_header`, `paywall_plans`, `paywall_cta`, `paywall_features`, and more                          |
| Survey     | `survey_question`, `survey_nps`, `survey_csat`, `survey_rating`, `survey_free_text`, `survey_thank_you` |
| Message    | `message_banner`, `message_modal`, `message_content`                                                    |
| Media      | `image_section`, `video_section`, `lottie_section`, `rive_section`                                      |

Sections are assembled in the Console and stream to the SDK through the config bundle. The React Native SDK does not need to know which sections a screen contains -- the native renderer reads the configuration and lays the screen out.

## Presentation Modes

Screens support several presentation modes configured per screen in the Console. The native renderer honors each mode on the appropriate platform:

| Mode           | Description                                               |
| -------------- | --------------------------------------------------------- |
| `fullscreen`   | Full-screen modal covering the entire screen (default).   |
| `modal`        | Standard modal sheet (page sheet style on iOS / Android). |
| `bottom_sheet` | Draggable bottom sheet.                                   |
| `push`         | Navigation push on iOS (within a navigation controller).  |

Unrecognized values fall through to `fullscreen` on Android. The React Native SDK does not need to pick a mode -- the Console controls the presentation per screen.

## Auto-Tracked Events

The native SDKs emit standard analytics events for every screen interaction. These events flow through the same event pipeline as other AppDNA events:

| Event                    | When                                 |
| ------------------------ | ------------------------------------ |
| `screen_presented`       | Screen appears                       |
| `screen_dismissed`       | Screen disappears                    |
| `screen_action`          | User taps a CTA inside a screen      |
| `flow_started`           | Multi-screen flow begins             |
| `flow_completed`         | Multi-screen flow finishes           |
| `flow_abandoned`         | Multi-screen flow dismissed early    |
| `slot_rendered`          | A named screen slot displays content |
| `slot_registered`        | A named screen slot first renders    |
| `interception_triggered` | Native navigation interception fires |

You do not need to forward or duplicate these events from TypeScript -- the native SDK already emits them.

## Configuration in Console

Each screen is configured in the AppDNA Console with:

* **Screen ID** -- the canonical identifier used by audience triggers and analytics events.
* **Layout** -- scroll, stack, or grid container that wraps the section list.
* **Sections** -- an ordered list pulled from the unified section registry.
* **Presentation** -- `fullscreen`, `modal`, `bottom_sheet`, or `push`.
* **Trigger** -- event-based (`event_name` matching), schedule-based (cron-style), or app-state-based.
* **Audience** -- targeting rules across user traits, device info, remote config, computed values, and experiment exposures.
* **Style** -- per-screen theme, typography overrides, brand colors, and animation defaults.

Publish the screen and it is delivered to all SDKs on the next config refresh.

## Full Example

```typescript theme={null}
import { AppDNA, AppDNAScreenDelegate } from '@appdna-ai/react-native-sdk';

class ScreenCoordinator implements AppDNAScreenDelegate {
  start(): void {
    AppDNA.screens.setDelegate(this);
  }

  onScreenPresented(screenId: string): void {
    console.log(`Screen shown: ${screenId}`);
  }

  onScreenDismissed(screenId: string, result: Record<string, unknown>): void {
    const responses = result.responses as Record<string, unknown> | undefined;
    const purchased = responses?.purchased === true;
    if (purchased) {
      this._unlockPremium();
    }
  }

  onFlowCompleted(flowId: string, result: Record<string, unknown>): void {
    const screens = (result.screens_viewed as string[]) ?? [];
    console.log(`Flow ${flowId} completed, screens viewed:`, screens);
  }

  onScreenAction(screenId: string, action: Record<string, unknown>): boolean {
    const type = action.type as string | undefined;
    console.log(`Screen ${screenId} action: ${type}`);
    return true;
  }

  private _unlockPremium(): void {
    // Grant access in your own state management.
  }
}
```

Wire it into your app startup:

```typescript theme={null}
declare class ScreenCoordinator {
  start(): void;
}

const coordinator = new ScreenCoordinator();

export async function bootstrap(): Promise<void> {
  await AppDNA.configure('adn_live_xxx', 'production');
  await AppDNA.identify('user-123');
  coordinator.start();
}
```

<Note>
  Screens are delivered via the same Firestore-backed config bundle as other SDK modules. Ensure your app has network access on first launch to fetch the latest screen configurations. After the first successful fetch, the native SDKs cache the bundle and continue rendering screens offline.
</Note>
