Skip to content

Tutorial: Show a Trial-Ending Countdown

In this tutorial, you’ll show a trial-ending banner that counts down the final days of a user’s trial and prompts them to upgrade before it expires. Takes about 10 minutes.

A trial countdown that:

  • Fires only in the final window of a trial (the last 3 days)
  • Renders the days remaining via the {{days_remaining}} token
  • Links to checkout with an upgrade CTA
  • A React 18+ application with @revturbine/sdk installed
  • A RevTurbineProvider set up (see Quickstart)
  1. Add a trial-ending placement to your config

    The trial_ending trigger fires when a trial enters its closing window. This is a complete, valid ExportedConfig.

    exported_config.json
    {
    "version": "v1",
    "plans": [
    { "id": "plan_pro", "unique_handle": "pro", "name": "Pro", "tier_position": 1, "sort_order": 1 }
    ],
    "entitlements": [],
    "entitlement_rules": [],
    "segments": [],
    "content_ui_paths": [],
    "surface_templates": [
    { "id": "banner_placement", "surface_type": "banner", "fields": [] }
    ],
    "placements": [
    {
    "id": "pl_trial_ending",
    "name": "Trial ending countdown",
    "category": "trials",
    "trigger": { "type": "trial_ending", "days_before_end": 3 },
    "payloads": [
    {
    "id": "pl_trial_ending_p0",
    "target": { "plan_ids": [], "segment_chips": [] },
    "surfaces": [
    {
    "template_id": "banner_placement",
    "fields": {
    "header": "Your trial ends in {{days_remaining}} days",
    "body": "Upgrade now to keep your projects and Pro features.",
    "dismissible": "Yes"
    },
    "ctas": [
    { "label": "Upgrade to Pro", "path": "open_checkout", "config": { "purchase": "pro" } }
    ]
    }
    ],
    "caps": { "max_per_period": { "count": 1, "period": "day" } },
    "status": "active"
    }
    ],
    "order": 0
    }
    ]
    }
  2. Provide trial status to the SDK

    The SDK reads trial state from a resolver in local mode (or from the user context in server mode). Supply the trial so the trigger and {{days_remaining}} token can compute the window.

    src/App.tsx
    <RevTurbineProvider
    options={{
    localRuntime: {
    exportedConfig,
    // In local mode, hydrate trial status synchronously:
    resolvers: {
    getTrialStatus: () => ({ active: true, daysRemaining: 2, plan: 'pro' }),
    },
    },
    user: { id: 'user_123', context: { plan_handle: 'trial' } },
    }}
    >
    <AppHeader />
    </RevTurbineProvider>
  3. Render the banner slot

    src/components/AppHeader.tsx
    import { FixedSurfaceSlot } from '@revturbine/sdk';
    export function AppHeader() {
    return (
    <header>
    {/* Hidden until the trial enters its final 3 days */}
    <FixedSurfaceSlot id="app_header_banner" surfaceTemplateIds={['banner_placement']} />
    <nav>{/* ... */}</nav>
    </header>
    );
    }
  4. (Optional) Read trial status directly

    To drive your own UI — a sidebar countdown, a settings notice — read trial status imperatively:

    import { useRevTurbine } from '@revturbine/sdk';
    function TrialNotice() {
    const { sdk } = useRevTurbine();
    const trial = sdk?.getTrialStatus();
    if (!trial?.active) return null;
    return <p>{trial.daysRemaining} days left in your trial</p>;
    }
  1. The trial_ending trigger keeps the placement hidden until the trial is within days_before_end of expiring.
  2. {{days_remaining}} is filled from the trial status the SDK holds.
  3. As the trial advances day by day, the banner copy updates automatically.
  4. The CTA opens checkout through the resolvers from Quickstart.

Interactive Playground → Banner Slot