React app
React apps can use Plandalf two ways. For simple checkout buttons, load the browser SDK once and render data-plandalf-present in JSX. For app-wide state, gates, and hooks, use the React package.
Note. Start with attributes, graduate to hooks The Numi share-code generator recommends attributes for the first install because the SDK auto-discovers elements even when React renders them after page load.
Before you start
- Create a checkout, add the product and price, and publish it in the intended test account.
- Copy the organization’s public SDK URL and the actual offer slug from its share code. Load that SDK once in your HTML shell. Do not put an API secret or a server signing key in React, a public environment variable or your source repository.
- Verify the actual Stripe test connection, account, price, amount and currency before entering payment details. The
testattribute requests test mode; it does not prove that a sandbox provider is connected. - For a countdown, create a promo with the intended deadline and price tiers. For referrals, complete the affiliate quickstart in the same organization.
Keep account access in your password manager and use a controlled test buyer inbox. The organization SDK URL, offer slug and partner code are public configuration; passwords and API secrets are not.
<!-- Replace this with the public script URL from your own account. -->
<script src="https://YOUR_ORGANIZATION.plandalf.dev/js/plandalf-sdk.js" defer></script>| Product | Add to React | Check the result |
|---|---|---|
| Checkouts | Button or inline checkout | Correct offer and price, then matching sandbox payment and paid invoice |
| Timers / Promos | Countdown and matching checkout promo | Persistent visitor deadline, tier transitions and actual paid amount |
| Affiliates | Partner landing page and optional badge | Correct partner on the paid sale, refund and applicable renewal |
| Automations | A supported destination after purchase | Actual access, message or destination record for the same paid invoice |
The path
-
Add the organization SDK to your shell document, or let
PlandalfProvidermanage the SDK instance. -
Use
data-plandalf-presentfor a trigger button ordata-plandalf-mountfor inline checkout. -
Call
identify()or pass per-calluserwhen checkout must know the logged-in customer. -
Subscribe to events for app behavior
Use FlowHandle events when the React app needs to update UI after checkout state changes.
Choose an integration style
-
Attribute-first — Best for marketing CTAs, pricing screens, and simple in-app upgrade buttons.
-
React package — Best when the app needs hooks, gates, context, and shared SDK state.
Attribute-first
export function UpgradeButton() {
return (
<button
data-plandalf-present="pro-plan"
data-plandalf-frame="modal"
data-plandalf-mode="test"
>
Upgrade
</button>
);
}For inline checkout, mount into a stable container.
export function CheckoutPanel() {
return (
<div
data-plandalf-mount="pro-plan"
data-plandalf-mode="test"
data-plandalf-apply-promo="early-bird"
style={{ minHeight: 480 }}
/>
);
}Buttons that render after page load
The browser SDK observes newly added elements, so a button may appear after a route change or data fetch. It reads its offer and promo when it first binds the element. If either changes, give the component a new key so React creates a new element. Updating the attributes on the same bound element is not sufficient.
import { useEffect, useRef } from "react";
function BuyButton({ offer, promo }) {
const button = useRef(null);
useEffect(() => {
const element = button.current;
return () => plandalf.from?.(element)?.close();
}, []);
return (
<button ref={button} data-plandalf-present={offer}
data-plandalf-mode="test" data-plandalf-apply-promo={promo || undefined}>
Open checkout
</button>
);
}
// Render after the SDK is ready. Changing the key replaces the bound element.
<BuyButton key={`${offer}:${promo || ""}`} offer={offer} promo={promo} />Register readiness with plandalf("ready", sdk => { /* enable the component */ }) after the script is available. If you register before loading the script, use the queue snippet from your account’s share code. plandalf.from(element) returns the handle associated with that element; it can be absent before checkout opens.
Close the checkout before replacing the button or leaving its route. For inline checkout under React Strict Mode, prefer an effect that calls sdk.mount() and returns handle.close(); keep the target element stable. React runs an extra setup/cleanup cycle in development, so verify both mounting and cleanup.

Local React replay with an existing expired test participant. The countdown displays its expired state while the configured Standard tier remains available. No referral or payment is claimed.
- Mount the button after SDK readiness, then open checkout.
- Check the resolved tier separately from the countdown's expired message.
Own an inline checkout in an effect
Use a stable container and close the handle when its component unmounts. The sdk below is the instance received from the ready callback. Replace the keyed component when its offer or promo changes.
import { useEffect, useRef } from "react";
function InlineCheckout({ sdk, offer, promo }) {
const target = useRef(null);
useEffect(() => {
const handle = sdk.mount(target.current, offer, {
mode: "test",
...(promo ? { promo } : {}),
});
handle.catch(error => console.error("Checkout could not load", error));
return () => handle.close();
}, [sdk, offer, promo]);
return <div ref={target} style={{ minHeight: 480 }} />;
}
<InlineCheckout key={`${offer}:${promo || ""}`}
sdk={sdk} offer={offer} promo={promo} />Warning. Inline cleanup release check Local replay found that inline close() used the modal cleanup path. The local repair now destroys the inline frame and settles its cancellation events, including a close requested before loading finishes. This repair is unreleased. Confirm your deployed SDK includes it before relying on this cleanup pattern.
With that repair, local production and development Strict Mode examples each rendered one inline checkout and removed it when the handle closed, while retaining the host container. Production route navigation and browser Back were also replayed. These checks used an existing draft offer; they do not prove a published purchase or delivery.
Changing a keyed component replaces its SDK binding. An empty promo prop does not clear a promo query parameter on the page: the checkout can still use that URL context. Verify the actual campaign on the resulting session when testing prop changes.
Add a countdown and promo
For a widget that stays in the app shell, add the promo element and apply the same slug to the checkout trigger:
<div data-plandalf-promo="YOUR_PROMO_SLUG" />
<button data-plandalf-present="YOUR_OFFER_SLUG"
data-plandalf-apply-promo="YOUR_PROMO_SLUG" data-plandalf-mode="test">
Buy now
</button>The widget attribute renders the countdown. The checkout attribute passes the promo into checkout; a visual timer alone does not apply a price. Start with the offer’s own items. Supplying an explicit data-plandalf-price follows a different price-selection path and must be verified separately.
For an effect-owned widget, use sdk.promo(slug, { target: element, redirect: false }) and close its handle during cleanup. Give a shared promo one owner: closing its handle closes the monitors and widgets using that handle. Use data-plandalf-no-redirect="" on a companion trigger when the effect owns the monitor; otherwise the attribute scanner also starts automatic promo monitoring. redirect: false deliberately disables host-page redirects in this example.
Warning. Countdown cleanup release check The React replay found and locally repaired a countdown cleanup defect. That repair has not been released. Before using effect-owned countdowns on routes that unmount, confirm your SDK includes it and test rapid mount, unmount and remount with a slow network. Keep this path pending until your deployed build passes those checks.
Warning. Server-resolved tier release check A second local repair refreshes the server-resolved promo at its reported deadline. Previously, the host could keep its first tier after checkout advanced. This repair is unreleased. Confirm it is in your deployed SDK and replay the deadline before relying on host tier events.
With the local repair, a fresh synthetic visitor progressed from Early to Standard to Full. A separate checkout stayed open across both deadlines; after closing it, the host refreshed from Early to Full. Reloading the same visitor resolved Full again. The monitor pauses while checkout is open, so it can skip intermediate host events. These checks verify displayed state, not a charged amount at each tier.

Local production React replay against the repaired SDK. The event log records actual SDK transitions. The countdown is expired while the permanent Full tier remains available.
- Check the final resolved tier.
- Record the SDK events as each deadline passes.
Test the same visitor before and after expiry, including a reload and a checkout held open across the deadline. Compare the deadline, tier, displayed amount, paid invoice and processor amount. An always-active final tier may still sell at its normal price after the countdown expires.

Local checkout opening proof. The Standard tier displays USD 50. Payment Setup Required blocks a purchase in this account; payment and fulfilment remain unverified.
- Match the line item to the intended offer and tier.
- Check the total before payment and reconcile the charged amount afterwards.
- Resolve the provider setup before attempting payment.
Track affiliate referrals
- Load the same organization’s SDK on the landing route before the buyer opens checkout. Use an approved test partner’s actual link, such as
https://your-app.example/pricing?via=PARTNER_CODEwhen the program uses the default parameter. - Optionally render
<div data-plandalf-referral="" />to display the captured referrer. An absent badge or a captured click does not prove a paid sale. - Navigate through the React app, open the checkout and verify a real sandbox purchase. Match the partner and test status under Affiliates → Sales to the paid invoice.
- For a test partner, the calculated commission is a preview and is skipped with
test_or_preview; it is not payable earnings. Verify a refund, duplicate event and a paid subscription renewal without another partner click where applicable.
Native Plandalf checkout records its sale from the invoice. Do not also report that purchase through the sales API. A different processor or checkout integration needs its own verified reporting path. The partner’s via code and the buyer’s pf_ref promo reference have different purposes.
Check payment and attribution
Use Stripe’s documented test cards only after verifying the actual sandbox connection. Submit one successful purchase, finish any optional post-purchase steps, then reopen checkout from the same React button for a separate decline test.
In the local production React fixture, an existing published offer completed a USD 50 sandbox purchase. Invoice RCP-000050 and Stripe both recorded USD 50 paid. The sale was attributed to the dedicated test partner; its USD 15 commission was a preview, skipped as test_or_preview. Reprocessing that invoice’s affiliate calculation retained one sale and one commission.

Receipt from the React sandbox purchase. The invoice and Stripe payment agree on USD 50. The receipt was opened separately; this does not verify receipt email delivery or an automatic redirect.
- Match the receipt amount to the checkout and processor payment.
- Confirm the invoice is paid before checking delivery.
The same button reopened without a page reload. A separate declined attempt received zero, left its invoice unpaid and created no affiliate sale. The error below is the actual response from that attempt.

A declined sandbox payment must not become a paid invoice, commission or delivery grant. This attempt recorded zero received and no affiliate sale.
- Show the failure and let the buyer choose another payment method.
This verifies an existing setup and the promo’s final USD 50 price. Clean setup, earlier-tier purchases, refunds, renewals, delivery and the deployed SDK still need their own replay. The stale host label found during that run was subsequently repaired and replayed locally as described above. Verify the released SDK separately.
Verify delivery
Choose a supported automation or signed webhook for the actual delivery destination. Browser checkout events can update the UI, but they must not grant permanent membership, account permissions or paid downloads. Verify entitlement on your server against the paid purchase and current access state.
Record the destination result for the same invoice. Test delayed delivery and duplicate events without charging the buyer again. If the offer promises no automatic delivery, state what the buyer should receive and verify that outcome.
Replay the guide
Use a clean browser profile or a dedicated test origin with a synthetic participant and approved test partner. Record the SDK build, offer, promo, deadline, expected amount and resulting invoice. Do not include private tokens or buyer credentials in the report.
- Load the app without a checkout button. Wait for SDK readiness, then mount the button and open the intended offer.
- Close checkout, remove and remount the button, and open it again. Confirm one checkout frame and the same intended offer.
- Leave the route with checkout open. Confirm it closes; return and confirm the component works again. Test Strict Mode separately from your production build.
- Exercise the promo before and after expiry, reload, and compare the tier with the actual charged amount.
- Complete the provider, paid-invoice, referral and delivery checks above. Repeat decline, refund and subscription cases relevant to your offer.
Local React replays cover component rendering, cleanup, one successful sandbox purchase with test attribution, same-page reopening, a declined payment and server-resolved tier transitions. The full affiliate lifecycle, charged amounts across deadlines, destination delivery, deployed SDK and clean setup-to-purchase replay remain unverified. Use these checks before launch.
React package
These are v2 workspace examples, not instructions for the older public npm release. Read the package version note before using them; the organization-hosted script and attributes above are the current recommended install path.
import { PlandalfProvider } from "@plandalf/react";
export function Root() {
return (
<PlandalfProvider apiBase="https://acme.plandalf.dev">
<App />
</PlandalfProvider>
);
}import { usePresent } from "@plandalf/react";
export function UpgradeButton() {
const { present, presenting } = usePresent();
return (
<button disabled={presenting} onClick={() => present("pro-plan", { mode: "test" })}>
Upgrade
</button>
);
}import { useEffect } from "react";
import { usePlandalf } from "@plandalf/react";
export function IdentifyUser({ userJwt }: { userJwt: string }) {
const sdk = usePlandalf();
useEffect(() => {
sdk.identify(userJwt);
return () => sdk.reset();
}, [sdk, userJwt]);
return null;
}Replay the React component, one step at a time
This screenshot sequence was captured from the real local React example on 8 October 2026. It verifies the actions below against the current SDK, not clean account setup, a payment, fulfilment, or a released SDK. This replay did not configure a promo and displayed a $25 monthly product. The earlier $50 purchase used a promo tier; its amount does not verify this replay.
The reproducible example is in scripts/integration-factory/fixtures/react/ in the site repository. From that directory, run npm ci, then npm run dev; open the URL printed by Vite. Its own TypeScript configuration prevents it from depending on the parent Astro installation.
Open the example
Start the React fixture and open its local URL. The initial form is empty; it must not contain a private API key.

Enter public SDK configuration only.
- Enter the Organization SDK URL.
- Enter the published test offer slug.
Enter your public configuration
Paste your organization SDK URL and published test offer slug. The local host and offer in this screenshot are test-fixture values; use your own values. Leave optional promo and partner fields empty for the basic component check.

Leave the optional promo, partner and participant fields empty for the basic check.
- Confirm the public SDK URL.
- Confirm the offer slug.
- Choose Load example.
Load the SDK
Click Load example. Wait for Organization SDK ready before mounting a button. If this message never appears, stop and resolve the SDK URL or network error.

Wait for readiness before mounting the checkout action.
- Confirm Organization SDK ready.
- Choose Mount checkout button.
Mount the button
Click Mount checkout button. Verify Open test checkout appears and the mount count is one.

The mounted component exposes the checkout action and mount count.
- Choose Open test checkout.
- Confirm the button mounted once.
Open checkout
Click Open test checkout. Inspect the actual product, price and billing interval. This run showed Plandalf Checkouts Monthly, billed monthly, with $25.00 due today. Do not submit payment until the processor sandbox and intended price have been independently confirmed.

Inspect the real offer without submitting payment.
- Confirm the product and billing interval.
- Confirm the total due today.
- Use this control to close checkout.
Close checkout
Use the close control at the top right of the checkout. Verify the overlay disappears and the React component remains visible.

The component remains mounted after checkout closes.
- Choose Remove checkout button for the cleanup test.
Remove the button
Click Remove checkout button. Verify the buying button disappears and the page reports Button is not mounted.

Removal should delete the buying button while keeping the example active.
- Choose Mount checkout button to add it again.
- Confirm Button is not mounted.
Mount the button again
Click Mount checkout button again. Verify the button returns and the mount count is two.

A clean remount should restore one checkout action and increment the count once.
- Confirm Open test checkout returned.
- Confirm the mount count is two.
Leave the checkout route
Click Account route. The checkout component should disappear. This sample route does not grant purchased access.

Route navigation removes the checkout component and does not imply access.
- Account route is the active destination.
- Confirm the checkout route is unmounted.
Return with browser Back
Use the browser Back control. Verify the checkout controls return with the button initially unmounted. This checks the sample hash route; repeat with your application router before release.

Browser Back restores the route with the buying button initially unmounted.
- Confirm Checkout route is active again.
- The button starts unmounted and can be mounted again.
Related docs
-
React SDK — Review provider, hooks, gates, and direct SDK access.
-
Configure the SDK — Use frame, mode, promo, identity, metadata, and continuity options.
-
Live events — React to checkout lifecycle updates from the app.
Integration overview
See Plandalf with React for the integration overview.