# Mobana Mobana is a mobile analytics platform for iOS and Android apps. It covers the whole post-install lifecycle in a single SDK: install attribution & deeplinking, engagement analytics (DAU/MAU/stickiness), conversion tracking, and dynamic in-app flows. 3-10x more affordable than enterprise MMPs (Branch, Adjust, AppsFlyer, Kochava). Website: https://mobana.ai SDK: `@mobana/react-native-sdk` (npm) — React Native SDK available today Native iOS and Android SDKs are on the roadmap. REST API available for custom integrations on any platform (see /docs/api/*). ## Core Solutions ### Attribution & Deeplinking - Track which ads, campaigns, and referrals drive app installs - Full UTM parameter support (source, medium, campaign, content, term) - Web referrer domain tracking: automatically captures the referring domain (e.g., "facebook.com") — domain only, no full URL, GDPR-friendly - Custom deeplink data: two mechanisms — (1) `data` param: URL-encoded JSON for structured payloads, returned as typed `attribution.data` object; (2) any other URL params are captured automatically as `attribution.click_params` (flat string map) — useful for network click IDs (ttclid, fbclid, gclid) and simple string values like affiliate codes or pixel params, without needing JSON encoding - Matching: probabilistic (IP, timezone, screen size, language) + deterministic (Android Install Referrer API) - Confidence scoring on every match (0.0–1.0); only matches above 70% returned by default - Attribution records auto-deleted after 6 hours (privacy by design) - Custom domain support: proxy tracking links through your own domain (bypasses ad blockers, cleaner URLs) - Real-time analytics: UTM breakdowns, referrer domain breakdowns, platform distribution, geographic data, conversion rates - Privacy-first: no device IDs collected, no persistent fingerprinting, GDPR-compliant - Billing: only successful attribution matches are billable (clicks and failed matches are free) ### Deeplinks Mobana has two link paths — pick based on intent: - `/link` — **Acquisition.** Always runs through the web flow and instantly redirects to the App Store / Play Store. No app-open attempt (no timeout). Best for ad campaigns and new-user acquisition where a clean, uninterrupted store funnel matters. - `/deep` — **Deeplink.** Registered as a Universal Link / App Link so the OS opens the installed app instantly. Falls back to the store (with a 2–3 s app-open attempt via intent / url scheme) when the app is not installed. Best for re-engagement — unlock codes, referral links, push notifications, share flows. Both paths support the same parameters (`data`, UTM fields, custom params), write an attribution Click, and fire `onDeepLink` in the SDK. **How direct opening works on `/deep` (primary mechanism):** When a `/deep` link is tapped and the app is installed, the OS intercepts it via Universal Links / App Links and opens the app directly (no browser). Without UL setup, the redirect page fires a scheme URL: - iOS: fires `{iosBundleScheme}://{appId}.mobana.ai/deep?…` (e.g. `myapp://abc123.mobana.ai/deep?…`). The SDK receives it via Linking and fires `onDeepLink`. - Android: fires an `intent://` URL targeting the app's package name. The SDK receives the URL and fires `onDeepLink`. `/deep` handles every install state: - App installed + UL/App Link configured → OS opens app directly → SDK `onDeepLink` fires with `source: 'universal_link'` - App installed, no UL + scheme configured → redirect page fires scheme URL → SDK `onDeepLink` fires with `source: 'universal_link'` - App installed, scheme open fails or in-app browser → user goes to store, SDK probes server within 5 min → `onDeepLink` fires with `source: 'probabilistic'` - App not installed → user installs, opens, SDK matches via `/find` → `onDeepLink` fires with `source: 'deferred'` AND `getAttribution()` returns the same payload **Dashboard → App Settings → Deeplinks fields:** - `iosBundleScheme` (PRIMARY for iOS) — the custom URL scheme registered in the app's Info.plist (e.g. `myapp`). Mobana fires `myapp://` from the redirect page to open the app. - iOS Team ID (OPTIONAL) — enables Universal Links (AASA). Apple's CDN caches AASA for up to 7 days including empty/missing — add Team ID BEFORE shipping a build with the `applinks:` entitlement. - Android SHA-256 fingerprint(s) (OPTIONAL) — enables Android App Links (`assetlinks.json`). Needed only if you want OS-level URL interception (before the browser opens). NOT needed for intent:// redirect (which uses package name only). Mobana serves `apple-app-site-association` and `assetlinks.json` automatically. **Mobile app native setup (REQUIRED — the Mobana Expo plugin does NOT auto-configure these):** iOS URL Scheme registration (required for iOS direct opening): - Bare RN: add `CFBundleURLTypes` → `CFBundleURLSchemes` → `["{scheme}"]` to ios/YourApp/Info.plist. - Expo: add `"scheme": "{scheme}"` to app.json (top-level under "expo"). Android intent-filter (required for SDK to receive URL via Linking, and for App Links when using `/deep`): - Bare RN: add `` in AndroidManifest.xml with `android:scheme="https"`, `android:host="{appId}.mobana.ai"`, and CRITICALLY `android:pathPrefix="/deep"`. Without `pathPrefix`, any tap on the Mobana host (including `/find`, `/conversion`) may open the app instead of the browser. Scoping to `/deep` means only re-engagement links open the app; acquisition links (`/link`) always run through the web flow. `autoVerify` is safe without fingerprints (falls back gracefully to regular intent-filter); add SHA-256 fingerprints in the dashboard to activate full App Links. If using a custom endpoint, add a SEPARATE `` block for that host — do NOT combine two hosts in one filter (Android creates the cartesian product of all `` elements). - Expo: add to `expo.android.intentFilters`: `[{ action: "VIEW", autoVerify: true, data: [{ scheme: "https", host: "{appId}.mobana.ai", pathPrefix: "/deep" }], category: ["BROWSABLE", "DEFAULT"] }]`. For custom endpoints, add a SECOND entry. Optional — Universal Links / App Links (OS intercepts URL before browser opens): - iOS Associated Domains: Xcode → Signing & Capabilities → Associated Domains → `applinks:{appId}.mobana.ai`. Requires iOS Team ID in dashboard. - iOS dev loop: append `?mode=developer` to the applinks entry on debug builds — iOS fetches AASA directly, bypassing Apple's 7-day CDN cache. - Android App Links: `autoVerify` is already in the snippet above — just add SHA-256 fingerprints in the Dashboard and App Links activates automatically. After editing Expo config, regenerate native projects: `npx expo prebuild --clean`. **Expo Router apps (file-based routing) — REQUIRED extra step:** Expo Router evaluates every incoming URL against your routes and renders an "Unmatched Route" screen for Mobana links (the SDK consumes them via Linking; they are not app routes). Add `app/+native-intent.tsx` re-exporting the SDK's pre-routing filter: `export { redirectSystemPath } from '@mobana/react-native-sdk/expo-router';` Works out of the box for `*.mobana.ai`. For a custom endpoint, set `EXPO_PUBLIC_MOBANA_ENDPOINT` (the re-export reads it) or build it explicitly: `export const redirectSystemPath = createRedirectSystemPath({ endpoint: 'https://go.yourapp.com/da' })`. The module is exported from the `@mobana/react-native-sdk/expo-router` subpath and is dependency-free (no Expo Router import, no native modules). Bare React Native and React Navigation need NO such file. Attribution and `onDeepLink` fire regardless of this file — it only suppresses Expo Router's "Unmatched Route" screen for a clean UX. SDK API: `Mobana.onDeepLink(handler)` — subscribe to Mobana link events. Fires for every `/link` tap (scheme open, deferred, probe). `` for declarative usage. Branch on payload presence inside your handler — `event.data?.field` is the canonical signal that the tap should trigger in-app behaviour. ### Conversion Tracking - Track post-install events (signup, purchase, subscription, level completion, etc.) - Revenue attribution: attach monetary values to see actual revenue per campaign/source - Conversions auto-linked to original install attribution via install ID - Configurable conversion types: repeatable or one-time per install - Conversion funnel analysis (see below) - Attributed vs organic segmentation: compare how attributed users convert vs organic - Offline queueing: events queued locally and sent when connection restores - Conversion tracking is FREE with all plans (no additional cost) **Conversion funnels:** - Interactive funnel diagrams visualize complex multi-step user journeys from install to revenue - Configurable time windows: 6 hours, 24 hours, 7 days, 14 days, or 30 days - See exactly where users drop off between steps (e.g. install → signup → purchase) - Compare conversion rates between stages and attribute revenue to each step - Spot bottlenecks and optimize the path from install to conversion - Conversion velocity: time-to-conversion distribution curves per campaign (identify fast converters vs slow burners) ### Engagement Analytics - DAU / WAU / MAU (distinct installs with ≥1 foreground heartbeat in the last 1 / 7 / 30 days) - Stickiness (DAU ÷ MAU) — benchmark: 20%+ good, 50%+ exceptional - Time in App (average foreground minutes per DAU, 5-minute granularity) - Activity heatmap (day-of-week × hour-of-day, UTC) - Lifecycle / recency breakdown (new, current, at-risk, dormant) — leading indicator of churn - All engagement metrics cross-filterable by platform, country, source, medium, campaign, content - **Zero instrumentation:** the SDK starts sending a foreground heartbeat as soon as `Mobana.init()` runs. No events to wire up, no screens to tag, no `identify()` call. The heartbeat is gated by `setTrackingEnabled()` just like attribution and conversions. - 5-minute heartbeat cadence with server-side 4-minute debounce per (installId, day) → ~12 DB writes per active user per hour, regardless of client behaviour - Foreground only — background time does not count toward time-in-app - Heartbeat endpoint: `POST /activity` (see /docs/api/activity) - Engagement analytics is FREE on every plan (heartbeats are not billed). Available alongside attribution + conversions + flows on the same SDK init. - Roadmap (all powered by the same passive heartbeat — no new instrumentation required when these land): - **Retention cohorts**: Day-1 / Day-7 / Day-30 / Day-90 retention curves by install cohort, with side-by-side cohort comparison - **LTV by acquisition source**: average lifetime value per acquired user, broken down by source / medium / campaign (engagement × conversion revenue) - **Churn risk alerts**: proactive flagging of at-risk users on the lifecycle bar, plus email alerts when the at-risk slice grows faster than new installs - **Custom cohort builder**: define cohorts by any combination of UTM / country / app version / install date and compare DAU / stickiness / retention across them - **Session analytics**: sessions per user, time-between-sessions, session-length distribution ### In-App Flows (Dynamic Remote Flows) - Full-screen HTML/CSS/JS experiences displayed in a native WebView - Update flow content without app store releases — changes go live instantly - Use cases: onboarding, permission prompts, pre-paywall/upsell screens, surveys, NPS, feature announcements, promotional campaigns - Agentic AI flow builder: describe flows in plain English, AI generates complete HTML/CSS/JS - Templates, visual editor, or full custom code control - Inline mock simulator for real-time preview - Version history with instant rollback - A/B testing: run multiple flow versions in parallel, compare completion rates (Pro plan) - Flow journey maps (see below) - Native bridge API inside flows: haptics, permissions (notifications, ATT, location), sounds, URLs, app reviews, event tracking - Personalization: customize flow content based on attribution data, custom params from the app, platform, color scheme - Bi-directional communication: flows send data/events back to the app; app passes params into flows - Flows can also make API calls to external services **Flow journey maps:** - Visualize every path users take through multi-step flows - Journey diagram shows engagement at each step and where users drop off or dismiss - Compare flow versions side-by-side (A/B testing on Pro plan) - Track completion rates per step to identify friction points - Link flow completions to downstream conversions via sessionId for end-to-end funnel visibility ## Dashboard & Analytics - Real-time dashboards for attribution, engagement, conversions, and flows - Filter by source, campaign, medium, country, platform, date range - Revenue tracking and campaign ROI analysis - Interactive conversion funnel diagrams with configurable time windows (6h to 30d) - Engagement analytics tab: DAU/WAU/MAU stat cards with sparklines + period deltas, stickiness, time-in-app, DAU/WAU/MAU over time, stickiness over time, time-in-app over time, activity heatmap (dow × hour), lifecycle/recency breakdown, recent active users windows - Flow analytics: views, completions, dismissals, completion rates, per-step journey maps, version comparison - Conversion velocity distribution curves (time from install to each conversion event) - Configurable usage caps to control spend on overage ## Technical Architecture - Cloud service + lightweight client SDK + web dashboard - Single SDK for attribution, engagement, conversions, and flows - Privacy: GDPR-compliant, consent controls via `setTrackingEnabled()` (gates attribution, conversions, AND engagement heartbeats), no PII stored - Integration: ~5 minutes. Engagement analytics requires zero additional calls beyond `init()`. Conversions, attribution, and flows each add 0–1 method calls (`getAttribution` / `trackConversion` / `startFlow`). - Expo apps require a config plugin: add `"@mobana/react-native-sdk"` to the `plugins` array in `app.config.js` before building (see SDK reference) - WebView-based flows with injected JavaScript bridge for native access - Offline-capable: attribution cached, conversions queued, flows cached locally - Engagement heartbeats: 5-min foreground cadence, server-side 4-min debounce per (installId, day), silent-success — heartbeat failures never surface as errors ## Pricing All plans include the same SDK. Conversion tracking and engagement analytics are free on every plan. | Plan | Price | Attributions | Flow Views | AI Credits | Apps | Data Retention | |------|-------|-------------|------------|------------|------|----------------| | Free Trial | $0/mo (3 months) | 2,500/mo | 5,000/mo | 25 | 3 | 30 days | | Starter | $49/mo | 5,000/mo (then $0.01 each) | 10,000/mo (then $0.005 each) | 50 | 5 | 6 months | | Pro | $169/mo | 20,000/mo (then $0.0085 each) | 50,000/mo (then $0.004 each) | 250 | Unlimited | 12 months | | Scale | Custom | 100k+/mo | 250k+/mo | 500 | Unlimited | Unlimited | Pro plan adds: A/B testing for flows, team access, priority support. Scale plan adds: on-premise option, SLA guarantee, dedicated account manager. No credit card required to start. Usage caps configurable on all paid plans. ## Documentation & AI Context - Human documentation: https://mobana.ai/docs - Full docs index: https://mobana.ai/docs/index.json - AI-optimized SDK reference: https://mobana.ai/docs/ai-context?section=sdk - AI-optimized Flow Bridge reference: https://mobana.ai/docs/ai-context?section=flowBridge - All sections combined: https://mobana.ai/docs/ai-context --- # Mobana React Native SDK Reference Mobana is a lightweight React Native SDK for mobile attribution, conversion tracking, and in-app flows. Package: `@mobana/react-native-sdk` For non-React-Native platforms, use the REST API directly (see /docs/api/*). EXPO REQUIREMENT: If the project uses Expo, you MUST add `"@mobana/react-native-sdk"` to the `plugins` array in `app.config.js` (or `app.json`) before building. Without this config plugin, the Android Install Referrer native module will not be linked and deterministic attribution (confidence: 1.0) will not work. This step is required regardless of whether EAS Build or local builds are used. Does not work in Expo Go — use `expo-dev-client`. EXPO PLUGIN SCOPE (important): The Mobana Expo plugin auto-configures Install Referrer linkage and (when requested) permission-related native setup — Podfile, Info.plist usage strings, AndroidManifest permissions. The plugin does NOT auto-configure deeplink native config (iOS Associated Domains entitlement, Android App Links intent-filter) because it doesn't have access to the developer's `appId`. Developers must add these manually via `expo.ios.associatedDomains` and `expo.android.intentFilters` in app.json / app.config.js when using `Mobana.onDeepLink()` or sharing `/deep` URLs. See the Deeplinks setup block below. ## Integration Pattern 1. Install: `npm install @mobana/react-native-sdk @react-native-async-storage/async-storage` 2. **Expo only:** add the config plugin to `app.config.js`. If your flows use permission prompts, install `react-native-permissions` AND pass permissions to the Mobana plugin (the plugin handles native setup automatically — Podfile, Info.plist, AndroidManifest — but you still need the npm package installed): ```js // app.config.js — attribution-only or flows without permissions export default { expo: { plugins: ["@mobana/react-native-sdk"] } }; // app.config.js — flows with permission prompts export default { expo: { plugins: [["@mobana/react-native-sdk", { permissions: ["Notifications", "AppTrackingTransparency", "LocationWhenInUse"] }]] } }; ``` 3. Call `init()` once on app start — attribution is fetched automatically in the background by default 4. Optionally call `getAttribution()` when you need the attribution data (UTM params, deeplink data) 5. Call `trackConversion()` when users complete key actions (signup, purchase, etc.) 6. For flows: wrap your app in `` and call `startFlow()` Peer dependencies: - `@react-native-async-storage/async-storage` — required (for caching) - `react-native-webview` — optional, required for flows - `react-native-safe-area-context` — optional, improves safe area insets in flows - `react-native-permissions` — optional, for permission prompts in flows (both bare RN and Expo — on Expo, the Mobana config plugin handles native setup automatically, but the package must still be installed) - `react-native-haptic-feedback` — optional, for haptics in flows - `react-native-in-app-review` — optional, for app review prompts in flows - `react-native-geolocation-service` — optional, for location services in flows ## Methods ### init() ```typescript Mobana.init(config: MobanaConfig): Promise ``` Initialize the SDK. Must be called before any other method. | Param | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | appId | string | yes | — | App ID from the Dashboard | | appKey | string | yes | — | App key from Dashboard (sent as X-App-Key; regeneratable) | | endpoint | string | no | https://{appId}.mobana.ai | Custom API endpoint for domain proxying | | enableTracking | boolean | no | true | Set false to disable attribution and tracking (GDPR). Flows (startFlow/prefetchFlow) still work — no installId is sent so no trace is left server-side. | | autoAttribute | boolean | no | true | Fetch attribution in the background on init. Set false to delay until explicit getAttribution() call (e.g., GDPR consent first) | | autoDeepLinks | boolean | no | true | Enable the automatic deeplink pipeline (Universal Link listener, AppState probe). Set false to disable entirely (useful in test environments). | | debug | boolean | no | false | Enable debug logging | Minimal Example: ```typescript import { Mobana } from '@mobana/react-native-sdk'; Mobana.init({ appId: 'a1b2c3d4', appKey: 'your-32-char-hex-app-key' }); ``` Expo: also add the config plugin to `app.config.js` before building (required for Android Install Referrer). Attribution-only (or flows without permission prompts): ```js // app.config.js export default { expo: { plugins: ["@mobana/react-native-sdk"], }, }; ``` With flows that use permission prompts (notifications, location, ATT) — install `react-native-permissions` (`npx expo install react-native-permissions`) and pass permissions to the Mobana plugin (the plugin handles the native Podfile/Info.plist/AndroidManifest setup automatically): ```js // app.config.js export default { expo: { plugins: [ ["@mobana/react-native-sdk", { "permissions": [ "Notifications", "AppTrackingTransparency", "LocationWhenInUse", "LocationAlways" ] }] ], }, }; ``` Only include permissions your flows actually use. Valid values: `Notifications`, `AppTrackingTransparency`, `LocationWhenInUse`, `LocationAlways`. iOS usage description strings (Expo): The plugin injects default strings into Info.plist for each permission. The defaults are generic — override them in `expo.ios.infoPlist` to avoid App Store rejection, especially for ATT: ```js // app.config.js export default { expo: { ios: { infoPlist: { NSUserTrackingUsageDescription: "We use this to show you relevant content and measure ad effectiveness.", NSLocationWhenInUseUsageDescription: "We use your location to provide location-relevant features.", NSLocationAlwaysAndWhenInUseUsageDescription: "We use your location in the background to provide location-relevant features.", } }, plugins: [["@mobana/react-native-sdk", { permissions: ["AppTrackingTransparency"] }]] } }; ``` Plugin defaults (used if you don't set your own): NSUserTrackingUsageDescription → "This identifier will be used to measure effectiveness of our campaigns and deliver relevant content to you." | NSLocationWhenInUseUsageDescription → "This app needs access to your location." | NSLocationAlwaysAndWhenInUseUsageDescription → "This app needs access to your location in the background." WARNING: Apple frequently rejects apps with the generic NSUserTrackingUsageDescription default. Always customize it. iOS usage description strings (bare RN): Managed entirely by the developer. Add them directly to `ios/YourApp/Info.plist`. The SDK does not inject any defaults. ## Deeplinks — Native Setup Required ONLY if you want Mobana `/deep` URLs to open the app directly via Universal Links / App Links. Skip if you only need install attribution. The Mobana Expo plugin does NOT auto-configure these — `appId` is baked into the binary at build time and the plugin has no access to it. Prerequisites (always — set BEFORE shipping any build with `applinks:` entitlement; Apple caches a missing/empty AASA for up to 7 days): 1. Dashboard → App Settings → Deeplinks: - Set **iOS URL Scheme** (PRIMARY for iOS direct opening) — the custom URL scheme registered in Info.plist, e.g. `myapp`. Mobana fires `myapp://` from the redirect page to open the app directly. - Set **iOS Team ID** (OPTIONAL) — only needed if you want Universal Links (AASA). 10 alphanumeric chars from App Store Connect → Membership Details. - Set **Android SHA-256 fingerprint(s)** (OPTIONAL) — only needed if you want App Links (OS-level interception). Get from `keytool -list -v` or Play Console → App integrity. NOT needed for intent:// redirect. 2. Register iOS URL scheme in the app: - Bare RN: add `CFBundleURLTypes` entry to ios/YourApp/Info.plist with `CFBundleURLSchemes: ["{scheme}"]`. - Expo: add `"scheme": "{scheme}"` to app.json under the `expo` key. 3. Add Android intent-filter to launcher activity in AndroidManifest.xml: ```xml ``` `pathPrefix="/deep"` is CRITICAL — without it, Android fires the filter for the entire Mobana host and taps on `/find`, `/conversion`, etc. may open the app instead of the browser. For custom endpoints, add a SEPARATE `` block for that host. Do not combine two hosts in one filter — Android creates the cartesian product of all `` elements. Expo — app.json / app.config.js (alongside the Mobana plugin): ```json { "expo": { "scheme": "{scheme}", "android": { "intentFilters": [{ "action": "VIEW", "autoVerify": true, "data": [{ "scheme": "https", "host": "{appId}.mobana.ai", "pathPrefix": "/deep" }], "category": ["BROWSABLE", "DEFAULT"] }] }, "plugins": ["@mobana/react-native-sdk"] } } ``` After editing, run `npx expo prebuild --clean` to regenerate native projects. Expo Router (file-based routing) — REQUIRED if the app uses Expo Router. Expo Router routes every incoming URL and shows an "Unmatched Route" screen for Mobana links (which the SDK consumes via Linking, not as routes). Add a pre-routing filter at `app/+native-intent.tsx` using the SDK's `@mobana/react-native-sdk/expo-router` subpath: ```ts // app/+native-intent.tsx — default endpoint or EXPO_PUBLIC_MOBANA_ENDPOINT export { redirectSystemPath } from '@mobana/react-native-sdk/expo-router'; // Custom endpoint passed in code (no env var): import { createRedirectSystemPath } from '@mobana/react-native-sdk/expo-router'; export const redirectSystemPath = createRedirectSystemPath({ endpoint: 'https://go.yourapp.com/da' }); ``` `*.mobana.ai` is matched with no config. The subpath also exports `isMobanaUrl(path, { endpoint? })` if you need to compose with your own `+native-intent.tsx` rules. This is Expo-Router-only — bare RN and React Navigation do not route unknown URLs to an error screen, so they need no such file. The SDK still receives the URL via Linking either way; the filter only prevents Expo Router's "Unmatched Route" screen. Optional — Universal Links / App Links (skip the browser entirely): - iOS: Xcode → Signing & Capabilities → Associated Domains → `applinks:{appId}.mobana.ai`. Requires iOS Team ID in dashboard. For dev builds, append `?mode=developer` to bypass Apple's 7-day AASA CDN cache. - Android App Links: autoVerify is already in the snippet above — just add SHA-256 fingerprints in the Dashboard and App Links activates automatically. Verification (after install): - iOS scheme: use `xcrun simctl openurl booted "myapp://{appId}.mobana.ai/link?data=..."` to test the scheme directly. - Android intent://: `adb shell am start -a android.intent.action.VIEW -d "https://{appId}.mobana.ai/link?data=..." com.your.package`. - Dashboard → App Settings → Deeplinks shows direct links to the served AASA / assetlinks files plus official validators (Branch AASA Validator, Google Statement List Tester). Custom endpoints (proxying through your own domain): The reverse proxy MUST forward all SDK paths: `link`, `link/record`, `link/probe`, `find`, `conversion`, `activity`, `ping`, `flows/*`, plus `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json` from the bare host root (Mobana serves these per `{appId}.mobana.ai`). Missing `activity` causes the SDK heartbeat to receive an HTML response and log a JSON parse error. The auto-generated nginx / Apache templates in App Settings → Custom Endpoint already include all of these forwarding rules. See /docs/guides/custom-endpoints. Notes: - Attribution is fetched automatically in the background (autoAttribute: true by default). By the time you call getAttribution(), the result is usually already cached. - Conversions tracked before init are queued and sent after init completes. - Idempotent — calling multiple times is safe, updates config. - Call as early as possible (root component, app entry point). No need to await. ### getAttribution() ```typescript Mobana.getAttribution(options?: { timeout?: number }): Promise> ``` Retrieve attribution data for this install. Never throws. | Param | Type | Default | Description | |-------|------|---------|-------------| | timeout | number | 10000 | Timeout in milliseconds | Returns `AttributionResult` object: | Field | Type | Description | |-------|------|-------------| | status | 'matched' | 'no_match' | 'error' | 'matched' = attribution found. 'no_match' = organic install. 'error' = network/server failure or SDK misconfiguration (check error.type) | | attribution | Attribution | null | Attribution data. Only present when status is 'matched' | | error | { type, status? } | undefined | Error details when status is 'error'. type: 'network' | 'timeout' | 'server' | 'sdk_not_configured' | 'tracking_disabled' | 'unknown' | `attribution` fields (when status === 'matched'): | Field | Type | Description | |-------|------|-------------| | utm_source | string? | Traffic source (facebook, google, tiktok, etc.) | | utm_medium | string? | Marketing medium (cpc, social, email, etc.) | | utm_campaign | string? | Campaign name | | utm_content | string? | Ad content identifier | | utm_term | string? | Search keywords | | referrer_domain | string? | Referring domain (e.g., "facebook.com") | | data | T? | Structured custom deeplink payload from the `data=` JSON param on the redirect URL | | click_params | Record? | All other URL params captured at redirect (network click IDs like ttclid/fbclid/gclid, affiliate codes, pixel params, etc.) | | confidence | number | Match confidence 0.0–1.0. 1.0 = deterministic (Android Install Referrer) | Example: ```typescript const { status, attribution } = await Mobana.getAttribution(); if (attribution) { console.log('Source:', attribution.utm_source); } else if (status === 'error') { // Log to Sentry, Datadog, etc. // SDK will retry next call, or you can schedule retry } ``` Notes: - Results are cached in memory and AsyncStorage. Multiple calls are cheap. - With autoAttribute: true (default), result is usually already cached by the time you call getAttribution(). - On network/server/unknown errors, result is NOT cached — the next call retries automatically. sdk_not_configured and tracking_disabled errors are also not cached. - Use TypeScript generics for type-safe custom data: `getAttribution()` ### trackConversion() ```typescript Mobana.trackConversion(name: string, value?: number, flowSessionId?: string): Promise ``` Track a post-install conversion event linked to attribution. | Param | Type | Required | Description | |-------|------|----------|-------------| | name | string | yes | Conversion type (must be configured in Dashboard). Use snake_case. | | value | number | no | Revenue value (e.g. purchase amount) | | flowSessionId | string | no | Session ID from startFlow() result, for funnel analysis | Example: ```typescript Mobana.trackConversion('purchase', 49.99); Mobana.trackConversion('signup'); // After a flow: Mobana.trackConversion('purchase', 49.99, result.sessionId); ``` Notes: - Never throws — silently handles errors. - Only tracked for attributed installs (organic installs are ignored). - Offline-safe: queued in AsyncStorage, sent when connection restores. - Conversion types must be configured in Dashboard → App Settings → Conversions. - Conversions feed into the Dashboard's interactive funnel analysis (configurable time windows: 6h to 30d). - Pass `flowSessionId` to link conversions to specific flow presentations for end-to-end funnel visibility. ### startFlow() ```typescript Mobana.startFlow(slug: string, options?: FlowOptions): Promise ``` Display an in-app flow in a full-screen modal. Requires `MobanaProvider`. | Param | Type | Required | Description | |-------|------|----------|-------------| | slug | string | yes | Flow's unique identifier (slug) from the Dashboard | | placement | string | no | Where in the app the flow is triggered (analytics only). E.g. `'post_signup'`, `'paywall_cta'`. Max 64 chars, pattern `/^[a-z0-9_-.:/]+$/i`. Attached to every event (started/completed/dismissed/custom). Does not affect targeting. | | params | Record | no | Custom data available in flow via `getParams()` | | onEvent | (event: string) => void | no | Callback fired when flow calls `trackEvent()` | | onCallback | (data) => Promise> | no | Async callback when flow calls `requestCallback()`. Allows flow to request app actions (purchases, validation) without closing. | Returns `FlowResult`: | Field | Type | Description | |-------|------|-------------| | completed | boolean | True if user completed the flow | | dismissed | boolean | True if user dismissed the flow | | error | FlowError? | Error code if flow couldn't show | | data | Record? | Data passed to `complete(data)` by the flow | | sessionId | string? | Unique session ID for this presentation | | trackEvent | function? | Track events after flow closes | Error codes: `NOT_FOUND`, `PLAN_REQUIRED`, `FLOW_LIMIT_EXCEEDED`, `PROVIDER_NOT_MOUNTED`, `SDK_NOT_CONFIGURED`, `NETWORK_ERROR`, `SERVER_ERROR` Example: ```typescript const result = await Mobana.startFlow('onboarding', { params: { userName: user.name, isPremium: user.isPremium }, onEvent: (eventName) => analytics.track(eventName), }); if (result.completed) { console.log('Completed with data:', result.data); } else if (result.dismissed) { console.log('User dismissed'); } else if (result.error) { console.log('Error:', result.error); } ``` Notes: - Never throws. Check `result.error` for issues. - Flow content is cached locally for offline access and faster display. - Version-aware: automatically fetches updates when flow is republished. Version stickiness (A/B testing): - Multiple published versions of a flow can be marked active simultaneously to run A/B tests. - First call: server weighted-randomly picks one of the active versions and returns its full content. SDK caches the chosen `versionId` locally. - Subsequent calls: SDK sends the cached `versionId`. If still active, server responds with `{ cached: true }` (no payload) and SDK renders from cache. Same install stays on the same variant. - Retired variant: if the cached version is deactivated in the dashboard, the server reassigns the install on the next call. SDK caches the new `versionId`. - Offline: SDK renders cached version when the server is unreachable — stickiness survives network failures. - Stickiness is keyed to the SDK install (per-device), not a logged-in user. `Mobana.reset()` clears the cached versionId, forcing fresh weighted-random assignment on the next call. ### prefetchFlow() ```typescript Mobana.prefetchFlow(slug: string): Promise ``` Preload flow content for instant display. Optional — flows work fine without prefetching. | Param | Type | Required | Description | |-------|------|----------|-------------| | slug | string | yes | Flow slug from Dashboard | Example: ```typescript // Prefetch during app startup for instant display later await Mobana.init({ appId: 'a1b2c3d4', appKey: 'your-app-key' }); Mobana.prefetchFlow('onboarding'); Mobana.prefetchFlow('push-permission'); ``` Notes: - Silent on failure (network error, flow not found). - Non-blocking — safe to call multiple times without awaiting. - Only prefetch flows the user is likely to see. ### setTrackingEnabled() ```typescript Mobana.setTrackingEnabled(enabled: boolean): void ``` Enable or disable attribution and tracking dynamically. For GDPR consent flows. Flows (startFlow/prefetchFlow) are NOT affected — they work regardless of tracking consent. When tracking disabled: - `getAttribution()` returns `{ status: 'error', error: { type: 'tracking_disabled' } }` - `trackConversion()` is a no-op — conversions are silently dropped, not queued. Any pre-opt-out pending conversions in the local queue are also discarded immediately. - `startFlow()` and `prefetchFlow()` work normally — flow requests omit installId so no session data is recorded - No attribution or conversion network requests made When re-enabled: - Attribution and conversion tracking resume for future events only — no backfill - No installId is generated or stored until tracking is re-enabled Example: ```typescript // User opts out of tracking Mobana.setTrackingEnabled(false); // User opts back in Mobana.setTrackingEnabled(true); ``` ### reset() ```typescript Mobana.reset(): Promise ``` Clear all stored data and generate a new install ID. Use for account deletion or testing. Clears: install ID, attribution cache, conversion queue, flow cache, local data from flows. WARNING: Irreversible. Device will be treated as a new install. ### getInstallId() ```typescript Mobana.getInstallId(): Promise ``` Returns the device's install ID — a random UUID generated on first launch and persisted locally. This is the identifier Mobana uses server-side for attribution and conversion records. Useful for GDPR data access/deletion requests. Can be called before or after `init()`. ### onDeepLink() ```typescript Mobana.onDeepLink(handler: (event: DeepLinkEvent) => void): () => void ``` Subscribe to Mobana link delivery events. Fires for every `/link` or `/deep` tap (Universal Link, deferred first launch, re-engagement probe). Returns an unsubscribe function. Multiple handlers are supported; each fires independently. `DeepLinkEvent` shape: | Field | Type | Description | |-------|------|-------------| | url | string | Raw URL delivered by the OS or returned by the probe (empty string for 'deferred') | | data | T? | Typed payload from the `?data=` JSON param | | utm | { source?, medium?, campaign?, content?, term? } | UTM params from the URL | | clickParams | Record | Any extra URL params (affiliate IDs, click IDs, etc.) | | source | 'universal_link' | 'probabilistic' | 'deferred' | How the event was delivered | | timestamp | number | Unix ms when the event fired | Sources: - `universal_link` — OS opened the app via Universal Link / App Link (app was installed) - `probabilistic` — SDK probed the server and found a matching click (UL was blocked by in-app browser, or the user opened the app store and tapped Open) - `deferred` — synthesized from `getAttribution()` result when a matched install carries a payload and no UL was delivered (covers fresh installs) Branch on payload presence: `event.data?.field` (or UTM fields) is the canonical signal that the tap should trigger in-app behaviour. Vanilla campaign taps still call your handler — just ignore them. Example: ```typescript interface MyPayload { unlock?: string; ref?: string } const unsub = Mobana.onDeepLink((event) => { if (event.data?.unlock) applyUnlock(event.data.unlock); if (event.data?.ref) trackReferral(event.data.ref); // No payload? It was a plain UTM-only tap — nothing to do here. }); ``` Notes: - Late subscriber buffering: if the event already fired before your handler subscribed, it is replayed within a ~30s grace window. - iOS double-delivery dedup: iOS may deliver the same UL via both `getInitialURL` and the `url` event — the SDK deduplicates within a 50ms window. - When `enableTracking: false`: `onDeepLink` still fires (payload delivery works), but no server record is created. - Requires deeplink setup in Dashboard → App Settings → Deeplinks (iOS Team ID + Android SHA-256 fingerprint) for the OS to open the app directly. Without it, links still work — they just fall through to the web flow. ## MobanaProvider ```tsx void modalProps?: Partial loadingComponent?: ReactNode backgroundColor?: string | { light: string; dark: string } colorScheme?: 'light' | 'dark' | 'auto' > {children} ``` React provider that enables flow display. Required for `startFlow()`. Not needed if only using attribution and conversions. | Prop | Type | Description | |------|------|-------------| | children | ReactNode | Your app content | | onDeepLink | (event: DeepLinkEvent) => void | Declarative deeplink handler — sugar over `Mobana.onDeepLink()`. Automatically subscribed/unsubscribed on mount/unmount. | | modalProps | Partial | Custom Modal props (animationType, statusBarTranslucent, etc.) | | loadingComponent | ReactNode | Custom loading component. Default: ActivityIndicator | | backgroundColor | string | { light: string; dark: string } | Background color shown while the flow loads. Static string for fixed color; object form auto-switches with system theme. Default: #FFFFFF (light) / #1c1c1e (dark). | | colorScheme | 'light' | 'dark' | 'auto' | Override the color scheme for the flow modal. Default: 'auto' (follows system). Set to 'light' or 'dark' for apps that force a specific theme. Affects backgroundColor resolution, iOS status bar style, and Android nav bar icon color. | Place near root, outside navigation container: ```tsx import { MobanaProvider } from '@mobana/react-native-sdk'; export default function App() { return ( {/* Your screens */} ); } ``` How it works: Provider creates a React Context. When `startFlow()` is called, it opens a full-screen Modal with a WebView that renders the flow's HTML. The JS bridge enables communication. When user completes/dismisses, Modal closes and Promise resolves. --- # Mobana Flow Bridge Reference ## What Are Flows Flows are full-screen HTML experiences displayed inside a WebView in a React Native app. Use them for onboarding, permission requests, feature announcements, surveys, upgrade prompts, and paywalls. The `window.Mobana` bridge object is injected before your scripts run and provides access to native capabilities: haptics, permissions, event tracking, and flow control. ## How to Build Effective Mobile Flows ### Design Directives - One goal per flow. Onboarding OR permissions OR upsell — never all at once. - Lead with value before asking. Explain what the user gets BEFORE requesting a permission, signup, or purchase. - Always provide an exit. Include "Skip", "Maybe later", or a close button. Track skips as events. - Show progress in multi-step flows. Use step indicators (dots, numbers, progress bars). - Use haptic feedback for important interactions: `haptic('light')` for taps, `haptic('success')` for completions, `haptic('error')` for failures, `haptic('selection')` for picker changes. - Every flow MUST call `complete()` or `dismiss()` to close. Otherwise the user is trapped. ### Event Tracking Strategy **Automatic lifecycle events (SDK-managed — NEVER fire these from flow code):** The SDK tracks flow lifecycle at the native layer, outside the WebView: - `__started__` — fires when flow modal is presented (before any flow JS runs) - `__completed__` — fires when `complete()` is called (after flow closes) - `__dismissed__` — fires when `dismiss()` is called (after flow closes) Do NOT add `trackEvent('flow_started')`, `trackEvent('paywall_viewed')`, `trackEvent('flow_dismissed')`, or similar — these duplicate built-in system events. **What to track with `trackEvent()`** — committed decisions where the user moves forward: - Step progression: `step_1_completed`, `step_2_completed` - Confirmed choices: `plan_confirmed`, `interests_confirmed`, `theme_set` - Permission outcomes: `notification_granted`, `notification_denied`, `att_authorized` - Deliberate exits: `onboarding_skipped`, `upgrade_declined` **What NOT to track:** - Flow open/close/dismiss (already __started__/__completed__/__dismissed__) - Toggleable/cyclical interactions (option taps, toggle switches, tab changes) — these create loops in journey diagrams. Track the forward step that commits the selection instead. - Micro-interactions (scrolls, hovers, every tap) - Events that rename system events (e.g. 'paywall_viewed' duplicates __started__) Use snake_case. Ask: "If this event count changes, would I change the flow?" If no, skip it. Complete funnel: `__started__` → [your committed-decision events] → `__completed__` or `__dismissed__` ### Permission Request Pattern The "pre-permission" pattern significantly increases grant rates: 1. Explain the value ("Get daily tips to reach your goals") 2. Show benefits list 3. Primary button: "Enable" → call `requestNotificationPermission()` 4. Secondary button: "Maybe Later" → track skip event, continue flow 5. Handle both granted and denied gracefully — never block on denial ### Personalization - Use `getParams()` for data passed from the app (user name, subscription state, feature flags). - Use `getAttribution()` for campaign-specific content (show premium offer for premium_promo campaign). - Use `getPlatform()` for platform-specific UI (ATT explanation on iOS only). ## SDK-Injected Defaults The SDK automatically applies these to every flow (do NOT duplicate in flow code): **Viewport:** The SDK ensures `viewport-fit=cover` is present on the viewport meta tag for edge-to-edge rendering on iOS. You don't need to add it yourself. **CSS resets** (injected **before** flow CSS, so flows can override any of them): ```css /* SDK reset (auto-injected, do NOT duplicate in flow code) */ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; } body { -webkit-font-smoothing: antialiased; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; overflow: hidden; } ``` What this means for flow code: - No need for a CSS reset or viewport-fit=cover — already applied - Body does not scroll by default. For scrollable content, use a scroll container: `.content { overflow-y: auto; height: 100vh; }` - Text is not selectable by default (native-app feel). To allow selection in specific areas: `.selectable { user-select: text; }` - To override any reset, just set the property in your flow CSS (it loads after the reset) ## HTML Structure Every flow is a single HTML file with this structure: ```html
``` ## CSS Variables Injected by the bridge onto `:root` **after** flow CSS (SDK values take precedence). Use these instead of hardcoded values. | Variable | Description | Example | |----------|-------------|---------| | --safe-area-top | Top inset (status bar, notch, Dynamic Island) | 47px | | --safe-area-bottom | Bottom inset (home indicator) | 34px | | --safe-area-left | Left inset | 0px | | --safe-area-right | Right inset | 0px | | --screen-width | Screen width in points | 393px | | --screen-height | Screen height in points | 852px | | --color-scheme | "light" or "dark" (also sets color-scheme property) | light | Dark mode approaches: - `light-dark(#fff, #1a1a1a)` — modern CSS function (recommended) - `@media (prefers-color-scheme: dark) { ... }` - `color-scheme: var(--color-scheme)` — enables native form styling Safe area usage: - Container padding: `padding-top: calc(var(--safe-area-top) + 16px)` - Fixed header: `padding-top: calc(var(--safe-area-top) + 16px)` - Fixed bottom button: `padding-bottom: calc(var(--safe-area-bottom) + 16px)` ## Bridge API Reference ### Data Access ``` Mobana.getAttribution(): Attribution | null ``` Returns: `{ utm_source?, utm_medium?, utm_campaign?, utm_content?, utm_term?, referrer_domain?, data?, confidence }` Returns null for organic installs. Synchronous. ``` Mobana.getParams(): Record ``` Custom params passed from app via `startFlow(slug, { params: {...} })`. Use for personalization. ``` Mobana.getInstallId(): string Mobana.getPlatform(): 'ios' | 'android' Mobana.getColorScheme(): 'light' | 'dark' ``` ``` Mobana.getSafeArea(): { top, bottom, left, right, width, height } ``` Values in points. Prefer CSS variables for layout. ``` Mobana.setLocalData(key: string, value: any): void Mobana.getLocalData(key: string): any ``` Persists in AsyncStorage across flow sessions. Cleared on `reset()` or app uninstall. Use case: resume onboarding where user left off. ### Flow Control ``` Mobana.complete(data?: Record): void ``` Close flow successfully. Resolves `startFlow()` with `{ completed: true, data }`. Automatically tracks `__completed__` system event. ``` Mobana.dismiss(): void ``` Close flow as dismissed. Resolves `startFlow()` with `{ dismissed: true }`. Automatically tracks `__dismissed__` system event. ``` Mobana.requestCallback(data?: Record, options?: { timeout?: number }): Promise> ``` Request the app to perform an async action and return a result. The flow stays open while the app processes. Requires `onCallback` to be provided when starting the flow via `startFlow()`. - `data`: arbitrary data sent to the app's `onCallback` handler - `options.timeout`: timeout in seconds (default: 300). Promise rejects on timeout. - Rejects if no `onCallback` handler was provided, if the handler throws, or on timeout. - Use case: trigger purchases, validate promo codes, fetch app-specific data — all without closing the flow. - The flow is responsible for its own loading UI while awaiting the result. Example (flow side): ```javascript try { const result = await Mobana.requestCallback( { action: 'purchase', planId: 'premium' }, { timeout: 120 } ); if (result.success) Mobana.complete({ purchased: true }); } catch (e) { // timeout, no handler, or handler error } ``` Example (app side): ```typescript await Mobana.startFlow('paywall', { onCallback: async (data) => { const purchase = await purchaseManager.buy(data.planId); return { success: purchase.success }; }, }); ``` ### Event Tracking ``` Mobana.trackEvent(name: string): void ``` Track custom event for analytics. Sent to server and to app's `onEvent` callback. Use snake_case: `step_1_completed`, `notification_granted`, `plan_selected`. Do NOT use names starting with `__` (reserved for system events). System events (tracked automatically by the SDK at the native layer — NEVER fire from flow code): - `__started__` — fires when flow modal is presented (before flow JS runs) - `__completed__` — fires when `complete()` is called - `__dismissed__` — fires when `dismiss()` is called Do NOT add trackEvent calls that duplicate these (e.g. 'paywall_viewed', 'flow_dismissed', 'flow_started'). ### Permissions ``` Mobana.requestNotificationPermission(): Promise Mobana.checkNotificationPermission(): Promise<{ status: 'granted'|'denied'|'blocked'|'unavailable', granted: boolean, settings? }> ``` ``` Mobana.requestATTPermission(): Promise<'authorized'|'denied'|'not-determined'|'restricted'> Mobana.checkATTPermission(): Promise ``` iOS only. On Android, both return `'authorized'` immediately. ``` Mobana.requestLocationPermission(options?: { precision: 'precise'|'coarse' }): Promise<'granted'|'denied'|'blocked'> Mobana.requestBackgroundLocationPermission(): Promise ``` Background requires foreground permission first. ``` Mobana.getLocationPermissionStatus(): Promise<{ foreground, background, precision }> Mobana.getCurrentLocation(): Promise<{ latitude, longitude, accuracy, altitude?, heading?, speed?, timestamp }> ``` ``` Mobana.openSettings(): void ``` Open app settings page. Use when permission status is `blocked`. ### Native Utilities ``` Mobana.haptic(style: 'light'|'medium'|'heavy'|'success'|'warning'|'error'|'selection'): void ``` Requires `react-native-haptic-feedback`. Falls back to Vibration API. ``` Mobana.playSound(url: string, options?: { volume?, loop?, onEnd?, onError? }): { isPlaying: boolean, stop(): void } ``` Accepts `https://` URLs or `data:audio/mp3;base64,...` data URLs. Keep sounds short (<100KB for base64). ``` Mobana.openURL(url: string): void ``` Opens in device default browser. ``` Mobana.requestAppReview(): void ``` Shows native app review dialog. WARNING: This COMPLETES the flow first due to iOS StoreKit limitation (modal must close before review dialog). Use as the final action only. Requires `react-native-in-app-review`. If unavailable, flow completes without review dialog.