startFlow()

Display an in-app flow in a full-screen modal. Requires MobanaProvider.

Method Signature#

Mobana.startFlow(slug: string, options?: FlowOptions): Promise<FlowResult>

Prerequisites#

Provider Required

Flows require MobanaProvider to be mounted in your component tree and react-native-webview installed.

App.tsx
import { MobanaProvider } from '@mobana/react-native-sdk';

export default function App() {
  return (
    <MobanaProvider>
      {/* Your app */}
    </MobanaProvider>
  );
}

Usage#

screens/Welcome.tsx
const result = await Mobana.startFlow('onboarding');

if (result.completed) {
  console.log('User completed the flow!', result.data);
  navigation.navigate('Home');
} else if (result.dismissed) {
  console.log('User dismissed the flow');
}

Parameters#

ParameterTypeDescription
slugRequiredstringThe flow's unique identifier (slug) from the Dashboard.
placementstringWhere in the host app this flow is triggered — used as an analytics dimension. Any consistent label works: 'post_signup', 'settings_upgrade_button', 'paywall_cta'. Max 64 characters, letters/digits/_/-/:/. only. Does not affect which flow or version is served.
paramsRecord<string, unknown>Custom parameters available in the flow via getParams().
onEvent(event: string) => voidCallback fired when the flow calls Mobana.trackEvent().
onCallback(data: Record<string, unknown>) => Promise<Record<string, unknown>>Async callback invoked when the flow calls requestCallback(). Allows the flow to request app actions (e.g., purchases) without closing.

Return Value#

PropertyTypeDescription
completedbooleanTrue if user completed the flow (called complete()).
dismissedbooleanTrue if user dismissed the flow (called dismiss()).
errorFlowError | undefinedError code if the flow couldn't be shown.
dataRecord<string, unknown> | undefinedCustom data passed to complete(data).
sessionIdstring | undefinedUnique session ID for this flow presentation. Use with trackConversion().
trackEvent(event: string, data?: Record<string, unknown>) => Promise<boolean>Function to track events after the flow closes.

Placement#

Pass a placement label to record where in the app the flow was triggered. The label is attached to every event emitted during that presentation (started, completed, dismissed, and any custom events), so you can filter and compare analytics by placement in the Mobana dashboard.

// Attach a placement label so this event is segmentable in analytics
const result = await Mobana.startFlow('onboarding', {
  placement: 'post_signup',
});

// Works alongside all other options
const result2 = await Mobana.startFlow('paywall', {
  placement: 'settings_upgrade_button',
  params: { plans: availablePlans },
});

Placement is analytics-only — it does not change which flow or version is served. Values that exceed 64 characters or contain disallowed characters are silently ignored.

Passing Parameters#

Pass custom data to your flow that's accessible via getParams():

const result = await Mobana.startFlow('welcome', {
  // Custom parameters available inside the flow
  params: {
    userName: user.name,
    isPremium: user.subscription?.active,
    features: ['feature1', 'feature2'],
  },
});

Event Callback#

Listen for custom events emitted by the flow:

const result = await Mobana.startFlow('onboarding', {
  params: { userName: 'John' },
  // Callback fired when flow calls Mobana.trackEvent()
  onEvent: (eventName) => {
    analytics.track(eventName);
    console.log('Flow event:', eventName);
  },
});

App Callback#

Allow the flow to request async actions from the app (e.g., trigger a purchase, validate a promo code) without closing. The flow calls requestCallback() and awaits the result:

const result = await Mobana.startFlow('paywall', {
  params: { plans: availablePlans },
  // Async callback when flow calls Mobana.requestCallback()
  onCallback: async (data) => {
    if (data.action === 'purchase') {
      const purchase = await purchaseManager.buy(data.planId);
      return { success: purchase.success, receipt: purchase.receipt };
    }
    if (data.action === 'validatePromo') {
      const promo = await api.validatePromo(data.code);
      return { valid: promo.valid, discount: promo.discount };
    }
    return { error: 'Unknown action' };
  },
});

If the flow calls requestCallback() but no onCallback handler was provided, the flow's Promise will reject. See requestCallback() for flow-side usage.

Error Handling#

const result = await Mobana.startFlow('premium-upsell');

if (result.error) {
  switch (result.error) {
    case 'NOT_FOUND':
      console.log('Flow does not exist or is not published');
      break;
    case 'PLAN_REQUIRED':
      console.log('Flows require a Pro plan');
      break;
    case 'FLOW_LIMIT_EXCEEDED':
      console.log('Monthly flow view limit reached');
      break;
    case 'PROVIDER_NOT_MOUNTED':
      console.log('MobanaProvider is not in the component tree');
      break;
    case 'SDK_NOT_CONFIGURED':
      console.log('Call Mobana.init() first');
      break;
    case 'NETWORK_ERROR':
    case 'SERVER_ERROR':
      console.log('Could not load flow, try again later');
      break;
  }
  return;
}

startFlow() never throws. Check result.error to handle issues gracefully.

Post-Flow Tracking#

Track events or conversions after a flow closes:

const result = await Mobana.startFlow('pre-purchase');

if (result.completed) {
  // Later, after user makes a purchase...
  
  // Option 1: Use trackEvent for flow-specific events
  await result.trackEvent?.('purchase_completed', { amount: 49.99 });
  
  // Option 2: Use trackConversion with sessionId for attribution
  Mobana.trackConversion('purchase', 49.99, result.sessionId);
}

Version Stickiness (A/B Testing)#

Multiple published versions of the same flow can be marked active simultaneously to run A/B tests. Once an install is assigned to a variant, it stays on that variant for the lifetime of the test — Mobana's stickiness model lives in the SDK's local content cache, not on the server.

How assignment works:

  • First call: the server weighted-randomly picks one of the active versions, returns its full content, and the SDK caches the chosen versionId locally.
  • Subsequent calls: the SDK sends the cached versionId to the server. If that version is still active, the server responds with { cached: true } (no payload over the wire) and the SDK renders from local cache. Same install, same variant — every time.
  • Variant retired: if you deactivate the cached version in the dashboard, the server reassigns the install to a still-active variant on the next call. The SDK caches the new versionId and that becomes the new sticky assignment.
  • Offline: if the server can't be reached at all, the SDK renders the cached version it already has — stickiness survives network failures.

To force reassignment (e.g. for QA when iterating on a test), clear the SDK cache:

// Clearing the SDK wipes the cached versionId, so the next
// startFlow() call gets a fresh weighted-random assignment.
await Mobana.reset();
Per-device, not per-user

Stickiness is keyed to the SDK install, not to a logged-in user. If the user reinstalls the app or switches devices, they may be reassigned to a different variant. For tests where this matters, mirror the assignment to your own user record using result.sessionId the first time the flow runs.

Caching#

  • Flow content is cached: After the first load, flows are cached locally for offline access and faster display.
  • Version-aware: The SDK automatically checks for updates and fetches new content when you publish changes — see Version Stickiness for how this interacts with A/B test variant assignment.
  • Prefetch for instant display: Use prefetchFlow() to load flows in advance.
AI agents: for complete Mobana SDK & API documentation, get full context here or visit llms.txt