Skip to main content
Supported on: iOS SDK 1.0.61+ · Android SDK 1.0.33+ · React Native SDK 1.0.4+

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

import { AppDNA } from '@appdna/react-native-sdk';

const result = await AppDNA.screen.show('upgrade_prompt');
if (result.dismissed) {
  console.log('User dismissed');
} else {
  console.log('Responses:', result.responses);
}
AppDNA.screen.show(...) returns a ScreenResult with dismissed (boolean) and responses (object). Lifecycle (onScreenPresented, onScreenAction, onScreenDismissed) also flows to the delegate registered via setDelegate(...) — see Screen Delegate below.

Show a Multi-Screen Flow

const result = await AppDNA.screen.showFlow('onboarding_v2');
if (result.completed) {
  console.log(`Completed: ${result.screensViewed.length} screens — ${result.screensViewed.join(' → ')}`);
} else {
  console.log(`Abandoned at screen ${result.lastScreenId}`);
}
AppDNA.screen.showFlow(...) returns a FlowResult with completed (boolean), screensViewed (string[] — ordered screen IDs viewed), lastScreenId (string | null), and responses (object). Multi-screen flows step through the screens you configured in the Console; the delegate’s onFlowCompleted also fires when the user finishes or abandons.

Dismiss

await AppDNA.screen.dismiss();

Screen Slots (Inline Content)

Place named slots in your app; the Console assigns screens to slots and the SDK renders them inline at runtime. Slots are addressed by ID and resolved on every config refresh.
  • Empty slots render nothing — no visual impact.
  • Content updates on the next config refresh.
  • Supports audience targeting per slot.
Slot IDs are configured in the AppDNA Console and rendered through the bundled native renderer at the matching screen-slot component in your app. Inject screens automatically between app navigations:
// Intercept every navigation
await AppDNA.screen.enableNavigationInterception();

// Or scope to specific routes (exact match or wildcard suffix)
await AppDNA.screen.enableNavigationInterception({
  forScreens: ['SettingsPage', 'Premium*'],
});

// To stop intercepting
await AppDNA.screen.disableNavigationInterception();
When interception is enabled, the SDK consults the registered AppDNAScreenDelegate on every navigation event so you can route or suppress as needed. Pass forScreens to limit interception to a specific allowlist — supports exact route names and trailing * wildcards.

Debug Preview

Render a screen from raw JSON for design or QA preview without publishing to the Console:
await AppDNA.screen.preview({
  id: 'test',
  name: 'Test',
  presentation: 'modal',
  layout: { type: 'scroll' },
  sections: [/* ... */],
});
Use during development only.

Module Access

Access the screen module through the AppDNA.screen namespace:
const screen = AppDNA.screen;
MethodSignatureDescription
showshow(screenId: string, options?: { context?: Record<string, unknown> }): Promise<ScreenResult>Present a server-driven screen by ID. Returns a ScreenResult.
showFlowshowFlow(flowId: string, options?: { context?: Record<string, unknown> }): Promise<FlowResult>Present a multi-screen flow. Returns a FlowResult.
dismissdismiss(): Promise<void>Dismiss the currently-presented screen
enableNavigationInterceptionenableNavigationInterception(options?: { forScreens?: string[] }): Promise<void>Inject screens between app navigations. Pass forScreens to scope to a specific allowlist (exact match or trailing * wildcard).
disableNavigationInterceptiondisableNavigationInterception(): Promise<void>Stop intercepting navigation
previewpreview(json: Record<string, unknown>): Promise<void>Render a screen from raw JSON for debugging
setDelegate`setDelegate(delegate: AppDNAScreenDelegatenull): void`Observe screen lifecycle from your TypeScript app

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:
import { AppDNA, AppDNAScreenDelegate } from '@appdna/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.screen.setDelegate(screenObserver);
Pass null to clear the delegate and stop listening:
AppDNA.screen.setDelegate(null);

Delegate Method Reference

MethodDescription
onScreenPresentedFires when a screen appears, alongside the screen_presented analytics event.
onScreenDismissedFires when a screen is dismissed. result carries dismissal context (e.g. dismissed, responses, last_action, duration_ms).
onFlowCompletedFires when a multi-screen flow ends. result.completed is true when the user reached the final screen, false when abandoned.
onScreenActionFires 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:
CategorySection Types
Genericcontent_blocks, hero, spacer, divider, cta_footer, sticky_footer
Onboardingonboarding_step, progress_indicator, navigation_controls
Paywallpaywall_header, paywall_plans, paywall_cta, paywall_features, and more
Surveysurvey_question, survey_nps, survey_csat, survey_rating, survey_free_text, survey_thank_you
Messagemessage_banner, message_modal, message_content
Mediaimage_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:
ModeDescription
fullscreenFull-screen modal covering the entire screen (default).
modalStandard modal sheet (page sheet style on iOS / Android).
bottom_sheetDraggable bottom sheet.
pushNavigation 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:
EventWhen
screen_presentedScreen appears
screen_dismissedScreen disappears
screen_actionUser taps a CTA inside a screen
flow_startedMulti-screen flow begins
flow_completedMulti-screen flow finishes
flow_abandonedMulti-screen flow dismissed early
slot_renderedA named screen slot displays content
slot_registeredA named screen slot first renders
interception_triggeredNative 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.
  • Presentationfullscreen, 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

import { AppDNA, AppDNAScreenDelegate } from '@appdna/react-native-sdk';

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

  stop(): void {
    AppDNA.screen.setDelegate(null);
  }

  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:
const coordinator = new ScreenCoordinator();

export async function bootstrap(): Promise<void> {
  await AppDNA.configure({ apiKey: 'YOUR_API_KEY' });
  await AppDNA.identify('user-123');
  coordinator.start();
}
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.