Next.js
Load your organization’s browser SDK once, wait for it to be ready, then render buying actions in client components. Keep payment verification and protected access on your server.
This guide’s browser replay uses Next.js 16.3.8 App Router and React 19.2.6. Pages Router and signed-in customer checkout need separate verification.
Before you start
- Create and publish a checkout in your intended test organization. Copy its offer slug and public SDK URL from the share code.
- Verify the actual payment sandbox, account, currency and price.
mode: "test"requests test mode; it does not prove provider isolation. - For a deadline, create a promo with the intended tiers. For referrals, complete the affiliate quickstart.
- Keep account credentials in an approved password manager. The SDK URL and offer/promo slugs are public configuration. API keys, signing secrets and private receipt URLs must stay out of client bundles, screenshots and replay reports.
| Product | Setup | Required result |
|---|---|---|
| Checkouts | Client button or inline target | Matching processor payment, paid invoice and promised delivery |
| Timers / Promos | Route-owned countdown and matching promo | Stable visitor deadline, correct tier and charged amount across expiry |
| Affiliates | SDK on the partner landing route | Correct paid sale, commission decision, refund and applicable renewal |
| Automations | Verified server-side delivery | Destination result for the same paid invoice, with retry and duplicate handling |
Load the SDK once
Put a client provider below your root layout. Use next/script with afterInteractive. Its onReady callback means the script loaded; the separate Plandalf ready callback supplies the initialized SDK. Next.js documents script placement and client-only callbacks.
// app/plandalf-provider.jsx
'use client';
import Script from 'next/script';
import { createContext, useContext, useState } from 'react';
const Context = createContext(null);
export const usePlandalf = () => useContext(Context);
export function PlandalfProvider({ children }) {
const [sdk, setSdk] = useState(null);
const [error, setError] = useState('');
return (
<Context.Provider value={sdk}>
<Script id="plandalf-sdk"
src="https://acme.plandalf.dev/js/plandalf-sdk.js"
strategy="afterInteractive"
onReady={() => plandalf('ready', value => setSdk(() => value))}
onError={() => setError('Checkout could not load. Please try again.')}
/>
{error && <p role="alert">{error}</p>}
{children}
</Context.Provider>
);
}// app/layout.jsx
import { PlandalfProvider } from './plandalf-provider';
export default function Layout({ children }) {
return <html lang="en"><body>
<PlandalfProvider>{children}</PlandalfProvider>
</body></html>;
}Replace the SDK URL with the one from your organization. Keep the provider at a shared layout level so client navigation can reuse it. Do not access window, open checkout, or create promo widgets during server rendering.
Add the checkout button
Render after SDK readiness and close the handle when the button unmounts. The SDK observes buttons added after hydration.
// app/checkout/page.jsx
'use client';
import { useEffect, useRef } from 'react';
import { usePlandalf } from '../plandalf-provider';
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>;
}
export default function CheckoutPage() {
const sdk = usePlandalf();
const offer = 'YOUR_OFFER_SLUG';
const promo = 'YOUR_PROMO_SLUG';
if (!sdk) return <p>Loading checkout…</p>;
return <BuyButton key={`${offer}:${promo}`} offer={offer} promo={promo} />;
}When the offer, promo, mode or frame changes, replace the bound element with a new React key that includes the changed setting. Changing an attribute on the same element leaves its original SDK binding in place. Omit the promo prop when you do not use a campaign; also check that the page URL does not supply another promo.
For programmatic checkout, use the ready SDK from the provider, retain the returned sdk.present() handle and close it when the owning route leaves. For inline checkout, use an effect with a stable target and sdk.mount(), returning handle.close() from cleanup. See the complete inline React example.

The real Next.js route opened this existing offer. This is an unsubmitted form; it does not prove payment or fulfilment.
- Match the item and tier to the intended offer.
- Verify the currency and total, then reconcile the processor payment after a sandbox purchase.
Add a countdown and promo
Use the same promo slug for the countdown and checkout. A countdown by itself does not apply a price. Mount the widget in an effect after both the SDK and target are available:
'use client';
import { useEffect, useRef } from 'react';
import { usePlandalf } from '../plandalf-provider';
export function Countdown({ promo }) {
const sdk = usePlandalf();
const target = useRef(null);
useEffect(() => {
if (!sdk) return;
const handle = sdk.promo(promo, {
target: target.current, redirect: false,
});
return () => handle.close();
}, [sdk, promo]);
return <div ref={target} />;
}Give each shared promo handle one owner. If this effect owns the monitor, add data-plandalf-no-redirect="" to its companion checkout button to avoid a second automatic redirect monitor. redirect: false deliberately keeps the visitor on the host page. Choose and verify your own expiry behavior before launch.
The local replay observed Early → Standard → Full for a fresh synthetic visitor. On returning to the route, the same visitor resolved Full. The configured permanent Full tier remains available while the countdown displays its expired message. Compare actual checkout and charged amounts separately.

Local production Next.js replay. The log records real SDK tier events and the badge shows a captured test referral. Neither is proof of an attributed paid sale.
- Mount the buying action after SDK readiness.
- Record the actual tier transitions for the same visitor.
- Check the captured partner, then verify the paid sale separately.
Handle route changes
Use ordinary next/link navigation and verify the actual router. Place handles in the page or route component that owns them; a shared root layout persists during navigation, so its effects will not clean up merely because a child route changes. If a persistent component opens checkout, explicitly close its handle when leaving that buying flow.
The production fixture passed these checks:
- The SDK loaded once and a button mounted after readiness opened the offer.
- Leaving the route with a modal open removed the checkout and countdown.
- Browser Back restored the route, visitor tier and referral; the shared layout counter stayed at 1.
- Inline checkout rendered, then disappeared on navigation to Account. Returning allowed another checkout to open.
Warning. Check the deployed SDK before launch This replay uses local SDK repairs for countdown ownership, inline cleanup and server-resolved tier refresh. These repairs are unreleased. A separate development Strict Mode replay also rendered one inline checkout, removed it on navigation, reopened a modal and removed that modal on route departure. Repeat cleanup with your deployed build and slow responses before launch.
Track affiliate referrals
Load the SDK on the initial partner landing route, before checkout. Use the approved partner’s actual URL, for example /checkout?via=PARTNER_CODE when the program uses the default parameter. The partner code and the buyer’s pf_ref promo reference serve different purposes.
An optional route-owned badge uses sdk.referral({ target: element, theme: 'light' }); close its handle on cleanup. The replay retained the captured test partner through client navigation and browser Back. A badge verifies referral capture only.
Complete a sandbox purchase and match Affiliates → Sales to the paid invoice. For a test partner, commissions are previews skipped as test_or_preview, not payable earnings. Verify duplicate events, refunds and paid subscription renewals where applicable. Native Plandalf checkout records its sale from the invoice; do not report the same purchase again through the sales API.
Check payment, decline and refunds
Use the provider’s documented sandbox cards with synthetic buyer details. Confirm the actual provider account is in test mode before submitting. A test-mode request alone is insufficient.
The historical 2 October 2026 local App Router replay completed a USD 50 sandbox purchase at the permanent Full tier. Receipt RCP-000051, the paid invoice and Stripe’s test payment all matched. The captured partner received one attributed test sale and a USD 15 commission preview, skipped as test_or_preview. Reprocessing the invoice retained one sale and one commission record.

Captured before refund. This receipt was opened separately and reconciled with the invoice and processor; it does not prove email delivery or an automatic receipt redirect.
- Match the receipt amount to the processor payment.
- The invoice was paid at capture time. Check current payment and refund records separately.
The same button then reopened checkout without a page reload. A declined test payment received zero, left its invoice unpaid and created no affiliate sale.

A real decline from the Next.js fixture. The backend and processor confirmed zero received and no attributed sale.
- Check the failure message, then confirm no paid invoice or sale was created.
A USD 10 partial refund followed by a USD 40 remainder refund succeeded in Stripe’s sandbox. Each actual provider charge object was passed through Plandalf’s normal refund action twice. Payment status progressed through partially_refunded to refunded; invoice and attributed-sale refund totals reached USD 50 with exactly two refund records. Automatic webhook delivery and signature verification were not exercised by this local action replay.
Warning. Refund verification remains incomplete The initial receipt replay exposed a stale “Paid” label after refund. A local presentation repair now shows “Refunded” in the same receipt and invoice preview. This repair is unreleased. Check current payment and refund records when deciding access or support outcomes. The skipped test commission created no payable ledger entry, so earned commission reversals, renewals and destination delivery still need verification.

Replayed after the local receipt display repair. The original purchase total remains $50; processor and refund records show it was fully refunded. The repair has not been deployed.
- The receipt summary reflects the recorded refund.
- The invoice preview now shows Refunded. Match it to the processor and refund totals.
Identify signed-in buyers
Follow the customer identity guide. Your authenticated server signs the Plandalf customer JWT; the browser receives only that token. Derive identity from the authenticated user, use the documented sub claim for your application user ID, and keep the signing key on the server. An auth provider’s own JWT is not automatically a Plandalf customer token.
Identity is optional for an anonymous checkout. This fixture does not implement sign-in or verify account switching; replay those flows separately before selling account access.
Verify delivery
Browser checkout events can update visible UI. Grant durable access only after your server verifies the paid purchase and current entitlement. Configure a supported automation or authenticated delivery endpoint and match its actual destination result to the invoice. Test delayed and duplicate delivery without creating a second charge.
Replay from setup to purchase
- Create the test checkout, connect the actual provider sandbox, and record the intended price and currency.
- Install the SDK provider and wait for readiness. Mount a button after hydration and inspect the opened offer.
- Navigate away while modal and inline checkout are open. Use browser Back, direct route loading and reload. Check cleanup and one SDK script.
- Use a fresh synthetic visitor and approved test partner. Verify the deadline and referral survive your actual route changes.
- Buy once in the verified sandbox. Reconcile processor payment, paid invoice, affiliate sale and promised delivery. Repeat decline, refund and renewal cases relevant to the offer.
- Repeat against the deployed SDK and published Next.js site. Record expected/actual results and sanitized screenshots for each step.
Local evidence covers SDK readiness, route cleanup, promo transitions, a USD 50 sandbox payment with test attribution, a decline, duplicate processing and partial/full refunds through the local reconciliation action. Clean setup, earlier-tier charges, expiry pricing, automatic webhook transport, delivery, payable commissions and renewals, Pages Router, identity and public deployment remain pending. The receipt display repair has passed a local replay but still needs release verification.
Replay the App Router controls, one step at a time
These real screenshots were captured on 8 October 2026 using Next.js 16.3.8, React 19.2.6 and the local organization SDK. This replay verifies readiness, modal and inline mounting, route cleanup, browser Back and retained layout state. It does not refresh the historical payment evidence above or verify a production SDK release, new referral attribution, payments or fulfilment.
To run the repository fixture, use its locked dependencies and production build. Set only public SDK configuration; do not put credentials in these variables. Port 4347 was used for this replay because another local fixture occupied 4346.
cd scripts/integration-factory/fixtures/nextjs
npm ci
NEXT_TELEMETRY_DISABLED=1 npm run build
PLANDALF_PUBLIC_SDK_URL=https://YOUR-PUBLIC-HOST/js/plandalf-sdk.js \
PLANDALF_PUBLIC_OFFER=YOUR-OFFER-SLUG \
NEXT_TELEMETRY_DISABLED=1 ./node_modules/.bin/next start --hostname 127.0.0.1 --port 4347The local screenshot host was flindev.numi.test; replace it with your own public SDK URL. No promo was configured during this replay. The older USD 50 purchase screenshots above describe a different offer state and must not be used to verify the current USD 25 monthly offer.
1. Open the production fixture
Open /checkout on the fixture server and wait for Organization SDK ready. This replay ran a production Next.js build. The displayed Plandalf test referral came from retained SDK browser state; it is not evidence of a newly captured referral.

Start on the checkout route and wait for SDK readiness.
- Confirm Organization SDK ready.
- Use the shared layout counter for navigation checks.
2. Set a value in the shared layout
Select Keep layout state: 0 once. Confirm it reads Keep layout state: 1. This value lets you distinguish client navigation from a fresh page load.

Increment the shared layout state once before navigating.
- Confirm Keep layout state reads 1.
3. Mount the checkout button
Choose Mount checkout button. Confirm that one Open test checkout button appears and the mounting control changes to Remove checkout button.

The mounting control changes and one checkout button appears.
- Choose Open test checkout.
- Remove checkout button confirms the component is mounted.
4. Open checkout
Choose Open test checkout and wait for the checkout to load. Compare the product and total with your intended offer. In this replay it showed Plandalf Checkouts Monthly, billed monthly, and $25.00 due today. No payment was submitted.

Inspect the real offer without submitting payment.
- Confirm the product and billing interval.
- Confirm the total due today.
5. Navigate away while checkout is open
Press Alt + A, the fixture’s shortcut to its Account route. Wait for the transition to finish. The checkout overlay should disappear, Current route should read /account, and the layout counter should remain 1. The fixture Account route grants no paid access.

Client navigation should close checkout while preserving shared layout state.
- Account route is the active destination.
- Confirm Current route is /account.
- Confirm the shared layout state remains 1.
6. Return using browser Back
Use the browser Back button. Confirm /checkout, Organization SDK ready, and layout counter 1. The checkout button is route-owned and must be mounted again.

Browser Back restores the route without restoring stale checkout UI.
- Confirm Checkout route is active.
- Confirm shared layout state is still 1.
- The inline checkout must be mounted again.
7. Mount checkout inside the page
Choose Mount inline checkout. Wait for the real form to appear in the page, then inspect its product and total. Scroll to see the complete form. This replay did not enter buyer or payment information.

Inspect the complete inline form and its removal control.
- Remove inline checkout confirms the form is mounted.
- Confirm the product and billing interval.
- Confirm the total due today.
8. Leave the inline checkout route
Choose Account route in the fixture’s navigation. Confirm the inline checkout is removed and the shared layout counter remains 1.

Navigating away removes the inline checkout and preserves layout state.
- Account route is active.
- Confirm Current route is /account.
- Confirm layout state remains 1.
9. Return to the checkout route again
Use browser Back. Confirm the initial mounting controls return and no old inline form remains.

The initial mounting controls return without an old inline form.
- Mount checkout button is available again.
- Choose Mount inline checkout.
10. Remount the inline form
Choose Mount inline checkout again. Wait for one checkout to appear. Check that its product and total match the first inline form.

The remounted route should contain one form with the same offer.
- Remove inline checkout confirms one form is mounted.
- Confirm the same product and billing interval.
- Confirm the same total.
11. Remove the inline form
Choose Remove inline checkout. Confirm the form disappears and Mount inline checkout returns. This verifies component cleanup in this production fixture; repeat against your application and the released SDK.

Removing the inline form restores the mounting control.
- Confirm Mount inline checkout returned.
Integration overview
See Plandalf with Next.js for the integration overview, or the React guide for shared SDK patterns.
