Appearance
Embed a Web Paywall
Host your plans and checkout in funnelsgrove, then display that paywall inside your web app with @funnelsgrove/sdk. Your frontend supplies the paywall URL and the buyer's funnelsgrove user ID or email. Opening the paywall needs no backend endpoint, secret key, or checkout session in your app.
Requirements
Use @funnelsgrove/sdk 0.4.0 or later with the matching funnelsgrove API release. Existing funnels must be rebuilt and republished with the embed-compatible runtime (0.19.0 or later) before you can embed them.
1. Host Your Paywall
- Create a funnel in funnelsgrove with your web plans and checkout, and configure its payment provider and offers.
- Publish it to your funnel domain and copy the URL of the paywall step. For example,
https://pay.example.com/paywall; use your actual published route. - Allow your app to embed that domain: the paywall host's
frame-ancestorspolicy must permit your app origin, and your app'sframe-srcpolicy must permit the paywall origin.
funnelsgrove hosts the paywall page and checkout UI. Your app only hosts the container. The published funnel supplies its own public key and configuration. Use an HTTPS paywallUrl without query parameters or a fragment; localhost HTTP is supported for development. The funnel's domain configuration determines Test/Live mode, so use an FG user ID from the same funnel and environment.
2. Mount It in Your App
Install the browser package:
sh
npm install @funnelsgrove/sdkAdd a container and an error message area to your app page:
html
<div id="paywall"></div>
<p id="payment-status" role="status"></p>Call this function after the elements exist, passing your signed-in account. The SDK creates the iframe and adjusts its height automatically.
js
import { mountPaywall } from '@funnelsgrove/sdk';
export function showPaywall(currentUser) {
const status = document.querySelector('#payment-status');
const paywall = mountPaywall({
container: '#paywall',
paywallUrl: 'https://pay.example.com/paywall',
...(currentUser.funnelsGroveUserId
? { userId: currentUser.funnelsGroveUserId }
: { email: currentUser.email }),
returnUrl: '/billing/complete',
onComplete: () => { status.textContent = 'Payment confirmed. Returning…'; },
onError: (error) => { status.textContent = error.message; },
});
// Call this cleanup when the route changes or the paywall modal closes.
return () => paywall.destroy();
}Choose the Buyer
| Input | What happens |
|---|---|
userId | Uses an existing FG public user_id. This is not your app's account ID or FG's internal database ID. An unknown ID fails without creating a user or falling back to email. |
email | Normalizes the email, then reuses the matching FG user or creates one. You do not need to create the user first. |
| Both | They must identify the same user; a mismatch fails without changing the user's email. |
The optional onReady: ({ userId }) => { … } callback returns the selected FG user ID. This identifies the checkout buyer; it does not prove account ownership or paid access. If you associate it with an app account, verify that association through your authenticated integration.
React / Next.js Example
Mount in an effect and destroy during cleanup. Pass the signed-in account's email and, when available, its FG ID. Next.js needs this client component:
tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import { mountPaywall } from '@funnelsgrove/sdk';
export function Paywall({ email, userId }: { email: string; userId?: string }) {
const container = useRef<HTMLDivElement>(null);
const [error, setError] = useState('');
useEffect(() => {
if (!container.current) return;
setError('');
const paywall = mountPaywall({
container: container.current,
paywallUrl: 'https://pay.example.com/paywall',
...(userId ? { userId } : { email }),
returnUrl: '/billing/complete',
onError: (failure) => setError(failure.message),
});
return () => paywall.destroy();
}, [email, userId]);
return (
<>
<div ref={container} />
<p role="status">{error}</p>
</>
);
}Invalid options or a missing container throw synchronously from mountPaywall. onError handles failures reported after mounting.
3. Return After Payment and Refresh Access
returnUrl is optional. After FG verifies a successful checkout, the SDK calls onComplete once per mount, then navigates the parent app window to that URL. A relative path such as /billing/complete resolves against your app page. Use HTTPS for absolute URLs; HTTP is supported only on loopback development hosts. Pending or failed payments do not redirect.
On your completion page, refresh the signed-in account's subscription state. Grant and revoke access from verified billing state, including later renewals and cancellations. A browser callback or visit to the return URL is only a signal to refresh; it is not proof of paid access. See subscription verification and Project Webhooks for the existing integration contracts and supported events.
For an SPA that controls its own navigation, omit returnUrl and handle onComplete in your router. When returnUrl is set, the SDK does not await async work in onComplete, and the redirect still runs if the callback throws.
Keep the parent page and paywall mounted while the payment provider redirects back to FG. If the parent is replaced or the return opens at the top level, reopen the paywall from your app. Solidgate redirect recovery also needs browser sessionStorage.
Cards, Apple Pay, and Google Pay
The embedded page uses your funnel's existing Stripe or Solidgate checkout. The SDK gives its iframe payment permission. Wallet availability also depends on the provider, browser, device, and domain configuration: register/verify both the app and funnel domains for Apple Pay and configure Google Pay with your provider. Test real wallets and 3DS on your intended HTTPS domains before going live.
Handle Payment Errors
error.code | What your app should do |
|---|---|
paywall_unavailable | Check the published URL, compatible funnel release, identity, and embedding policies, then reopen the paywall. |
confirmation_pending | Keep the paywall mounted. The buyer can use Check payment status to confirm the existing attempt. |
payment_failed | Keep the paywall mounted so the buyer can try again. |
Before launch, verify email-only entry, an existing FG ID, successful return, failed/pending payment, and closing/reopening the paywall in your provider's Test environment. Verify that your app updates access from billing state, then switch to the configured Live paywall and its corresponding identities.