Skip to main content

Payment widget

How to put a Flourish Payments form on your own website, for one-time payments, donations, memberships and subscriptions.

Overview​

The widget renders a multi-step payment form on any page. You describe the screens, fields, pricing and copy in one configuration object; there's no framework code to write. What the form collects goes to Salesforce through the public connector, and Stripe takes the payment on its own secure form inside the widget. What happens to the record afterwards is on Payments in Salesforce.

Version 1.3.1. Before any of this works, an admin has to have finished Setup, which gives you the connector page's URL.


Quick start​

1. Load the scripts​

The widget loads none of its own dependencies, so the page supplies all four: Vue 3, PrimeVue, the PrimeVue Aura theme, and Stripe.js. Which version of each you run is your decision, and a payment page is the wrong place for a library to change under you.

<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<!-- PrimeVue and its Aura theme -->
<script src="https://unpkg.com/primevue@4/umd/primevue.min.js"></script>
<script src="https://unpkg.com/@primeuix/themes@2/umd/aura.js"></script>

<!-- Stripe.js -->
<script src="https://js.stripe.com/v3/"></script>

<!-- The Flourish Payments widget -->
<script src="https://cdn.toflourish.org/payments/v1/payment-widget.js"></script>

There's no stylesheet to link. The widget carries its own CSS.

Keep PrimeVue on @4 and its theme on @2, as above. PrimeVue 5 and the version 3 theme need a PrimeUI license key, and without one they put an "Invalid PrimeUI License" badge on your page. Versions 4 and 2 are free.

If one of the four is missing or fails to load, the widget says so instead of rendering:

  • The visitor sees "The payment form could not be loaded. Please refresh the page, or contact us if it keeps happening."
  • The browser console names each missing library and the URL that supplies it.
  • create() returns null instead of throwing.

2. Add a place for it​

<div id="pay"></div>

3. Create the form​

<script>
FLPayments.create('#pay', {
text: {
title: 'Make a Donation',
description: 'Your support makes a difference.'
},
pricing: { mode: 'singleAmount', currency: 'usd' },
payload: {
staticValues: { itemName: 'Donation', paymentCurrency: 'usd' }
},
paymentsLwc: { siteUrl: 'https://your-org.my.site.com/yoursite/payment-connector' },
screens: [
{
id: 'amount',
sections: [{
fields: [{
name: 'amount',
type: 'preset-amount',
label: 'Choose an amount',
presets: [25, 50, 100, 250],
allowCustom: true,
customLabel: 'Or enter a custom amount',
defaultSelected: 50
}]
}]
},
{
id: 'donor',
role: 'submit',
sections: [
{
label: 'Your information',
columns: 2,
fields: [
{ name: 'firstName', type: 'text', label: 'First name', required: true },
{ name: 'lastName', type: 'text', label: 'Last name', required: true },
{ name: 'email', type: 'email', label: 'Email', required: true, span: 2 }
]
},
{
type: 'summary',
rows: [{ key: 'amount', label: 'Total', format: 'currency', emphasis: true }]
}
]
},
{ id: 'pay', role: 'checkout', sections: [{ type: 'checkout' }] },
{ id: 'done', sections: [{ type: 'confirmation' }] }
]
});
</script>

Three things in there are required:

  • paymentsLwc.siteUrl is the connector page your admin set up.
  • itemName and paymentCurrency go to Salesforce with every payment that doesn't sell a Stripe price. Salesforce refuses the payment without them, and the widget doesn't add them for you.
  • The amount comes first. Every screen checks that an amount has been chosen, so it has to be on the first screen (or turn the check off; see Validation).

The form above, on a page of its own:

The donation form's first screen: the title Make a Donation and its description, Choose an amount with $25.00, $50.00 (selected), $100.00 and $250.00, a custom amount box and a Next button The second screen after Next: Your information with first name, last name and email filled in, a summary reading Total $50.00, and Back and Next buttons

What the widget does to your page​

The form renders inside its own shadow DOM, so your page's CSS doesn't reach its inputs and buttons. A few things do cross that line:

  • Head styles are copied in. Every <style> tag in your page's <head> is copied into the widget, including ones added later. That's how PrimeVue's styles reach it, and your own head styles come along too.
  • Stripe's step sits outside it. Stripe's payment form goes in a <div id="fl-checkout-portal"> placed right after your element (fl-checkout-portal-2 for a second form on the page, and so on), because Stripe's bank-account flow needs a normal page to work. Its look comes from a style the widget adds to your page's <head>.
  • The viewport tag changes. It gains minimum-scale=1, or one is created if the page has none. Stripe's bank-account flow needs it on phones.

How the flow works​

A flow is an ordered list of screens. Each screen holds sections, and field sections hold fields. The visitor moves through the screens with Next and Back. The last screen has no Next button and usually shows a confirmation.

A screen's role decides what its Next button does:

RoleWhat Next does
(none)Checks the screen's visible fields, then moves on.
submitChecks the fields, sends everything to Salesforce, and moves on once Salesforce answers with a payment session.
checkoutShows Stripe's payment form. Next stays off until the payment completes.

A few rules worth knowing:

  • The checkout screen needs both parts: role: 'checkout' and a section of type checkout. Stripe's form appears only with both.
  • Only Stripe's form shows on the checkout screen. The title, other sections and messages are hidden there.
  • The Finish button: the Next button on the second-to-last screen reads text.finishButton; every other screen reads text.nextButton. In the usual flow that's the checkout screen.

Scrolling​

  • Changing screens: the widget scrolls to its own top. On the checkout screen it scrolls to Stripe's form.
  • A field appears: when a field appears because of showWhen and isn't fully in view, the widget scrolls just enough to show it.

Configuration reference​

All options go in the second argument to FLPayments.create(selector, config).

FLPayments.create('#pay', {
text: { /* labels and copy */ },
features: { allowBackNavigation: true, autoFinishCheckout: false },
pricing: { mode: 'singleAmount', currency: 'usd', amountFieldName: 'amount', adjustments: [] },
validation: { requireAmount: true },
state: { defaults: {}, pricingDefaults: {} },
payload: { method: 'createCheckoutSession', staticValues: {} },
stripe: { mountSelector: '#fl-checkout' },
paymentsLwc: { siteUrl: '...', iframeId: 'lwc-iframe', wrapperSelector: '.lwc-iframe-wrapper' },
connector: { timeoutMs: 10000, submitTimeoutMs: 30000, disabled: false, debug: false },
theme: { css: '', stylesheetUrl: '' },
hooks: { /* lifecycle callbacks */ },
screens: [ /* required */ ]
});

Smaller options:

OptionNotes
paymentsLwc.siteUrlThe connector page. It's always loaded over https, and your page's own URL parameters are passed on to it (see URL parameters).
connector.timeoutMsHow long to wait for the connector before showing text.connectorTimeout. Defaults to 10 seconds.
connector.submitTimeoutMsHow long Next waits for Salesforce to answer before showing text.submitTimeout. Defaults to 30 seconds.
connector.disabledtrue previews a layout without connecting. Nothing is sent to Salesforce, and submitting says payments are turned off.
connector.debugtrue logs every message to and from the connector in the browser console, under [FLPayments]. Useful when a page won't connect.
stripe.mountSelectorWhere Stripe's form goes. Must be an #id. Defaults to #fl-checkout, or #fl-checkout-2 and up for further forms on the page.
stripe.publishableKeySupplied by the connector. Set it yourself only with connector.disabled.
paymentsLwc.iframeId, paymentsLwc.wrapperSelectorLeave as they are unless they clash with your page. The wrapper must be a class selector.
testDataAn object copied into the form after everything else, for trying a page out. Don't ship it.

text​

KeyDefault
title''
description''
paymentDescription'', shown on the checkout screen
confirmationImageA built-in success image. Set '' for none.
confirmationHeading'Payment Complete'
confirmationDescription'Thank you for your payment.'
nextButton, finishButton, backButton'Next', 'Finish', 'Back'
connectorLoading'Loading secure payment system...'
connectorTimeout'Payment system is unavailable. Please refresh the page or try again later.'
connectorMissingSettings'Payment system is not configured. Please contact the site administrator.' Shown when the connector answers without a Stripe key.
submitTimeout'The payment system did not respond. Please try again.' Shown when Salesforce doesn't answer within connector.submitTimeoutMs.

Some wording can't be changed here:

  • Payments section labels: "Amount Due", "Total" and "Balance:".
  • The select placeholder: "Select..." (a field's own placeholder replaces it).
  • Validation messages (except through errorMessage).
  • During checkout: "Completing your payment…" and "Something went wrong."
  • Salesforce errors: an error from Salesforce is shown as written.

features​

KeyDefaultWhat it does
allowBackNavigationtrueShows the Back button on screens after the first.
autoFinishCheckoutfalseWhen true, the flow moves on by itself as soon as the payment completes, with a brief "Completing your payment…" message and no Finish button. Keep a screen after checkout, or the visitor is left on an empty card. When false, the visitor clicks Finish.

pricing​

pricing: {
mode: 'singleAmount', // or 'external'
currency: 'usd', // for display; see below
amountFieldName: 'amount', // must match the preset-amount field's name
adjustments: [
{
enabled: true, // required; an adjustment without it is ignored
type: 'percentage', // the only type
baseKey: 'baseAmount', // the default
targetKey: 'feeAmount', // required
percent: 0.029, // a 2.9% processing fee
applyWhen: (form) => form.coverFees === true // optional
}
]
}

Modes

  • singleAmount: the amount comes from the chosen preset or the custom amount, the enabled adjustments run, and the total is the amount plus the fee.
  • external: your code supplies the figures, and the widget only rounds them. Adjustments don't run.
    • Set the starting figures in state.pricingDefaults. To change them later, write to ctx.vm.pricingState from a hook.
    • With no amount, the total is baseAmount alone.

Pricing keys, for summary rows, showWhen and applyWhen checks, and hooks:

KeyMeaning
baseAmountThe chosen or entered amount, or the value your code set.
feeAmountWritten by adjustments. Each one overwrites its target, so the last enabled adjustment aimed at feeAmount wins.
amountThe total: baseAmount plus feeAmount.

An adjustment can write to a key of its own through targetKey. A summary row can show it, but it isn't added to the total and isn't sent to Salesforce.

Currency. pricing.currency only formats amounts on screen. Stripe charges in paymentCurrency from the payload, so set payload.staticValues.paymentCurrency to match.

Sections​

Each screen has an id (required), an optional role, and a list of sections. Set a section's type, or leave it out for fields. Every section accepts id, label, description and showWhen, a function (form, pricing) => true or false.

TypeWhat it shows
fieldsA grid of fields. columns: 2 for two columns.
summaryRead-only rows from the pricing keys. Each row takes key, label, format: 'currency' and emphasis.
contentA block of text, or of html you control, with an optional class.
imagesrc and alt, with an optional caption and class.
dividerA horizontal rule.
alertA callout with text or html. severity is 'info' (the default), 'success', 'warning' or 'error'.
paymentsThe payments of an existing Opportunity. Shows only with a payment model.
checkoutStripe's payment form. Goes on the role: 'checkout' screen.
confirmationThe success image, heading and description from text.
customA component you registered; see Custom components.

Fields​

Every field takes name (required), label, required (true, or a function), showWhen, defaultValue, errorMessage and tooltip. Text, email, currency, long text and select fields also take a placeholder. In a two-column section, span: 2 makes a field fill the row.

TypeNotes
preset-amountSee below.
textA text box.
emailA text box that checks the address format.
currencyA number formatted as money. min sets the floor (default 0).
textareaSeveral lines of text. rows defaults to 3.
selectA dropdown. Pass options: [{label, value}], or name other keys with optionLabel and optionValue.
radioA set of choices. Each option can carry a description.
checkboxA single checkbox, starting unticked. checkboxLabel sets the text beside it. A required checkbox must be ticked. It shows no tooltip.

preset-amount fields:

  • Buttons and custom amount: presets: [25, 50, ...] makes the buttons. allowCustom adds a box for another amount. defaultSelected picks a button to start with.
  • Copy: presetLabels renames individual buttons, for example { "100": "$100 Sustainer" }. customLabel and customPlaceholder set the custom box's copy.
  • min: the lowest amount accepted (default 1).
  • required: a chosen button or a typed amount both count. The amount check already covers this, so it mostly matters when that check is off.

Only the first preset-amount field on the form counts. It's the one defaultSelected and the amount check use, and its error shows above the buttons.

Showing fields conditionally​

required, showWhen and applyWhen can all be functions of the form:

{ name: 'company', type: 'text', label: 'Company',
showWhen: (form) => form.donorType === 'business',
required: (form) => form.donorType === 'business' }

A hidden field is never required or checked, but its value is still sent.

URL parameters​

Pre-filling the form. A URL parameter named flpaypre_ plus a field name fills that field when the page loads:

https://yoursite.org/donate?flpaypre_email=jane@example.com&flpaypre_firstName=Jane
  • Values arrive as text. flpaypre_coverFees=true sets the text "true", which a check like form.coverFees === true won't match.
  • Amounts fill the custom box. flpaypre_amount=100 fills the custom amount; it doesn't select the $100 button.
  • Any name is accepted, not just your fields, and goes to Salesforce with everything else. That includes names Salesforce acts on, such as priceId and mode. On a page where those matter, set them in payload.staticValues, which a URL can't override.

Paying an existing Opportunity. Your page's URL parameters are passed on to the connector, which reads two of its own: flpayoid, the Opportunity to pay, and flpaypid, a payment on it. See Payment models.

state​

state: {
defaults: { // starting field values, by field name
frequency: 'once',
coverFees: false
},
pricingDefaults: { // starting pricing, for 'external' mode
baseAmount: 0,
amount: 0
}
}

A field's starting value is decided in this order, with later steps winning:

  1. Its defaultValue, used only when state.defaults doesn't name the field.
  2. state.defaults.
  3. A flpaypre_ URL value.
  4. testData.

theme​

Two layers, applied in order, the later one winning:

  1. theme.stylesheetUrl: the address of a CSS file.
  2. theme.css: CSS as text.

Both apply inside the widget, so set the variables below rather than restyling its internals.

theme: {
css: `
:host { --flpay-brand: #c2185b; --flpay-brand-hover: #880e4f; }
`
}
VariableDefaultUsed for
--flpay-brand#0176d3Main buttons, links and selected states
--flpay-brand-hover#014486The Back button and preset buttons on hover
--flpay-brand-active#032d60The selected preset button's text
--flpay-focus#1b96ffThe focus ring
--flpay-surface#ffffffThe card's background
--flpay-surface-alt#fafaf9Summary rows
--flpay-border#c9c9c9Borders
--flpay-border-weak#dddbdaLight borders
--flpay-text#181818Text
--flpay-text-muted#5c5c5cSecondary text
--flpay-text-label#3e3e3cField labels
--flpay-radius-sm4pxRounding on inputs and buttons
--flpay-radius-md6pxRounding on cards
--flpay-shadow-card0 2px 3px rgba(0, 0, 0, 0.08)The card's shadow
--flpay-shadow-focus0 0 0 2px var(--flpay-focus)The focus ring's shadow
--flpay-widget-padding24pxSpace inside the card

Stripe's step isn't styled by these. It sits outside the widget (see What the widget does to your page). To restyle it, target #fl-checkout-portal (or .fl-checkout-portal for every form on the page) from your own page's stylesheet, with !important.

hooks​

Functions the widget calls as the visitor moves through the form:

hooks: {
beforeValidate: (ctx) => { /* ... */ },
afterValidate: (ctx) => { /* ... */ },
beforePayloadBuild: (ctx) => { /* ... */ },
transformPayload: (payload, ctx) => ({ ...payload, source: 'web' }),
onPaymentResponse: (response, ctx) => { /* ... */ },
onError: (error, ctx) => { /* ... */ }
}
HookWhen it runs
beforeValidateBefore each screen is checked. Throw an error to stop the visitor with your own message.
afterValidateAfter a screen passes its checks.
beforePayloadBuildOn submit, once the payload is built, just before transformPayload. Changing the form here doesn't change what's sent.
transformPayloadLast, with the payload. Return what to send. If it returns nothing, the payload as it now stands is sent.
onPaymentResponseAfter Salesforce answers and Stripe's form is showing.
onErrorWhen a screen fails its checks, Salesforce returns an error, or Salesforce doesn't answer in time. Not called when the connector fails to load.

Hooks run straight through and aren't waited on, so an async hook can't hold the form.

What ctx holds:

PropertyDescription
configThe whole configuration.
currentScreenThe current screen's id.
formStateField values, by name.
pricingStateA copy of the pricing keys. To change pricing, write to ctx.vm.pricingState.
customDataA shared place to keep data between hooks, screens and custom components.
selectedPresetAmountThe selected preset, if any.
vmThe widget's Vue instance, for advanced cases.

payload​

What's sent to Salesforce on submit, in this order, later entries winning:

  1. Every field's value, including hidden fields and any flpaypre_ values.
  2. amount, baseAmount and feeAmount from pricing, in dollars.
  3. The payment model selections, if there are any.
  4. payload.staticValues.
  5. Whatever transformPayload returns.

Leave payload.method as 'createCheckoutSession'; it's the only one the connector answers, and with any other name the form waits forever.

Names Salesforce acts on:

NameWhat it does
itemName, paymentCurrencyRequired unless you send priceId. Name what's being paid for, and set the currency Stripe charges in.
itemDescriptionA description on Stripe's receipt line.
email, firstName, lastName, companyNameFind or create the payer and their company.
amount, feeAmount, baseAmountThe payment amount.
mode, priceId, interval, intervalCountStart a subscription; see Subscriptions.
setupFutureUsage'off_session' saves the card so Salesforce can charge it later.
quantity, collectPhoneNumber, customerId, metadata, clientReferenceId, returnUrlPassed on to Stripe.

Every other name is kept on the Opportunity, in Payment Custom Data, for your Flows to read. See Payments in Salesforce.

The amount the form shows is a preview. Salesforce decides what's charged before calling Stripe; see Server-side pricing.


Payment models​

A payment model page collects against an existing Opportunity: an invoice, a balance due, or a set of installments. The page is opened with ?flpayoid=<Opportunity ID>. When that Opportunity has a Payment Model, a payments section shows what's owed:

Payment ModelWhat the visitor sees
Opportunity OnlyA read-only "Amount Due".
Summed PaymentsRead-only payment lines and their total.
Selective PaymentsA checkbox per payment, all ticked to start. The total follows the ticks.
Dynamic PaymentsAn amount box per payment, starting at 0 and capped at its balance. Salesforce refuses an amount of 0 or less, or above the balance.

Build these pages with validation: { requireAmount: false }. The payments total isn't part of the widget's pricing, so with the amount check on, submit fails with "Please choose or enter a valid amount." For the same reason, a summary row can't show the payments total.

Setting up payment models on records is on Payments in Salesforce.


Subscriptions​

A page starts a subscription instead of a one-time payment by sending mode: 'subscription'. The payer checks out on the same Stripe form. The price comes from one of two places:

SendWhat Stripe charges
priceIdA recurring Stripe price, such as one made from a Product on its record page. The price sets the amount, currency and billing period, so the page's own amount is ignored.
amount with intervalThe form's amount every interval: day, week, month (the default) or year. Add intervalCount for longer periods, so month with intervalCount: 3 bills quarterly. Needs itemName and paymentCurrency, like any payment without a price.

Send itemName with a priceId too: it names the Opportunity.

A fixed price. The form only collects the payer, so turn the amount check off:

FLPayments.create('#pay', {
validation: { requireAmount: false },
payload: {
staticValues: {
mode: 'subscription',
priceId: 'price_1QxYz...',
itemName: 'Monthly Giving Circle'
}
},
paymentsLwc: { siteUrl: '...' },
screens: [ /* payer fields on a submit screen, then checkout and confirmation */ ]
});

One-time or monthly, the visitor's choice. A value in staticValues can't follow the form, so decide in transformPayload:

hooks: {
transformPayload: (payload, ctx) => ctx.formState.frequency === 'monthly'
? Object.assign(payload, { mode: 'subscription', interval: 'month' })
: payload
}

What a subscription leaves on your records, and how renewals and cancellations work, is on Products, invoices and subscriptions.


Custom components​

Register a Vue component before calling create(), then use it in any flow:

FLPayments.registerComponent('donor-disclaimer', {
props: ['section', 'formState', 'pricingState', 'customData', 'config'],
template: '<p class="muted"><strong>Note:</strong> {{ section.note }}</p>'
});

// Then in your screens:
{ type: 'custom', component: 'donor-disclaimer', note: 'All donations are tax-deductible.' }
  • Props: a custom component receives section, formState, pricingState, customData and config, plus anything in section.props.
  • Names: use lowercase names with hyphens, and don't start them with p-.

Validation​

  • When: fields are checked when the visitor clicks Next. Hidden fields are skipped.
  • The amount check: with validation.requireAmount: true (the default), every screen's Next also checks the amount. It must be at least the first preset-amount field's min, or 1. Turn it off on pages where the amount comes from a Stripe price or a payment model.
  • Errors:
    • Each error shows under its field, except the amount error, which shows above the preset buttons.
    • The first problem scrolls into view.
    • A message at the bottom repeats a single error, or says "Please complete the highlighted fields."
  • Messages: a field's errorMessage replaces "This field is required."

A complete example​

A donation page with an optional processing fee:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Donate</title>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/primevue@4/umd/primevue.min.js"></script>
<script src="https://unpkg.com/@primeuix/themes@2/umd/aura.js"></script>
<script src="https://js.stripe.com/v3/"></script>
<script src="https://cdn.toflourish.org/payments/v1/payment-widget.js"></script>
</head>
<body>

<div id="pay"></div>

<script>
FLPayments.create('#pay', {
text: {
title: 'Support Our Mission',
description: 'Choose an amount and complete your donation.',
confirmationHeading: 'Thank You!',
confirmationDescription: 'Your donation has been received.'
},

screens: [
{
id: 'form',
sections: [
{
label: 'Donation Amount',
fields: [{
name: 'amount',
type: 'preset-amount',
label: 'Select an amount',
presets: [25, 50, 100, 250, 500],
defaultSelected: 50,
allowCustom: true,
customLabel: 'Or enter a custom amount',
min: 5
}]
},
{
label: 'Your Information',
columns: 2,
fields: [
{ name: 'firstName', type: 'text', label: 'First Name', required: true },
{ name: 'lastName', type: 'text', label: 'Last Name', required: true },
{ name: 'email', type: 'email', label: 'Email', required: true, span: 2 }
]
},
{
fields: [
{ name: 'coverFee', type: 'checkbox', checkboxLabel: 'Cover the 3% processing fee' }
]
}
]
},
{
id: 'review',
role: 'submit',
sections: [{
type: 'summary',
rows: [
{ key: 'baseAmount', label: 'Donation', format: 'currency' },
{ key: 'feeAmount', label: 'Processing Fee', format: 'currency' },
{ key: 'amount', label: 'Total', format: 'currency', emphasis: true }
]
}]
},
{ id: 'checkout', role: 'checkout', sections: [{ type: 'checkout' }] },
{ id: 'confirmation', sections: [{ type: 'confirmation' }] }
],

pricing: {
mode: 'singleAmount',
currency: 'usd',
adjustments: [{
enabled: true,
type: 'percentage',
targetKey: 'feeAmount',
percent: 0.03,
applyWhen: function (form) { return form.coverFee === true; }
}]
},

payload: {
staticValues: {
itemName: 'Donation',
paymentCurrency: 'usd',
campaignId: '701xx000000abcd'
}
},

paymentsLwc: { siteUrl: 'https://your-org.my.site.com/yoursite/payment-connector' },

theme: {
css: ':host { --flpay-brand: #2d6a4f; }'
}
});
</script>

</body>
</html>

campaignId isn't a name Salesforce acts on, so it lands in Payment Custom Data, where a Flow can read it.


Public API​

MemberWhat it does
FLPayments.create(selector, config)Builds the form in the element selector matches, and returns the Vue app. Call unmount() on it to remove the form. Returns null when a dependency is missing, and throws when nothing matches the selector or the element already holds a form.
FLPayments.registerComponent(name, definition)Registers a component for custom sections. Call it before create().
FLPayments.versionThe widget's version, such as '1.3.1'.
FLPayments.buildA short fingerprint of the exact file the page loaded, for telling a cached copy from a fresh one.
FLPayments.debug()Logs and returns what's on the page: the version, build, missing dependencies, and how many forms are showing. Useful when a page misbehaves.

The element you build into also carries data-fl-state, ready or error, and on error a data-fl-error that says what went wrong.

A page can hold several forms, each in its own element. Only one Stripe payment form can be open at a time, so opening it in one form closes it in another. To remove a form, call unmount() on the app create() returned; it takes its Stripe area and page styles with it.


Versioning​

AddressWhat it serves
https://cdn.toflourish.org/payments/v1/payment-widget.jsThe latest 1.x release. Use this one. A breaking change would come out under /v2/ and leave it alone.
https://cdn.toflourish.org/payments/v1/payment-widget.min.jsThe same, minified.

Browsers and the CDN keep a copy for about five minutes, so a new release reaches visitors shortly after it's published. If you host your own copy instead, put the success image beside it at images/pay_img_successSplash.png, or set text.confirmationImage.