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 at the widget's checkout step. What happens to the record afterwards is on Payments in Salesforce.
Version 1.4.0. Before any of this works, an admin has to have finished Setup. That's where the connector page's URL comes from.
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 couldn't load. 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()returnsnullinstead 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.siteUrlis the connector page your admin set up.itemNameandpaymentCurrencygo 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 put it on the first screen or turn the check off (see Validation).
This is the form above on a page of its own:
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. The widget copies in every
<style>tag in your page's<head>. It also copies 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-2for a second form on the page, and so on). 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:
| Role | What Next does |
|---|---|
| (none) | Checks the screen's visible fields and moves on. |
submit | Checks the fields, sends everything to Salesforce, and moves on once Salesforce answers with a payment session. |
checkout | Shows 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 typecheckout. 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's readstext.nextButton. In the usual flow, the second-to-last screen is 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
showWhenand 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:
| Option | Notes |
|---|---|
paymentsLwc.siteUrl | The connector page. It's always loaded over https, and your page's own URL parameters are passed on to it (see URL parameters). |
connector.timeoutMs | How long to wait for the connector before showing text.connectorTimeout. Defaults to 10 seconds. |
connector.submitTimeoutMs | How long Next waits for Salesforce to answer before showing text.submitTimeout. Defaults to 30 seconds. |
connector.disabled | true previews a layout without connecting. Nothing is sent to Salesforce, and submitting says payments are turned off. |
connector.debug | true logs every message to and from the connector in the browser console with an [FLPayments] prefix. Useful when a page won't connect. |
stripe.mountSelector | Where Stripe's form goes. Must be an #id. Defaults to #fl-checkout (#fl-checkout-2 and up for further forms on the page). |
stripe.publishableKey | Supplied by the connector. Set it yourself only with connector.disabled. |
paymentsLwc.iframeId, paymentsLwc.wrapperSelector | Leave as they are unless they clash with your page. The wrapper must be a class selector. |
testData | An object copied into the form after everything else. Use it for trying a page out. Don't ship it. |
text
| Key | Default |
|---|---|
title | '' |
description | '' |
paymentDescription | ''. Shown on the checkout screen. |
confirmationImage | A built-in success image. Set '' for none. |
confirmationHeading | 'Payment complete' |
confirmationDescription | 'Thank you for your payment.' |
nextButton, finishButton, backButton | 'Next', 'Finish', 'Back' |
connectorLoading | 'Loading the secure payment form…' |
connectorTimeout | "The payment form isn't available right now. Please refresh the page or try again later." |
connectorMissingSettings | "This payment form isn't set up yet. Please contact the site's administrator." Shown when the connector answers without a Stripe key. |
submitTimeout | "The payment system didn't respond. Please try again." Shown when Salesforce doesn't answer within connector.submitTimeoutMs. |
paymentNotOpen | "There's nothing to pay here. This payment may already be paid or canceled. Please contact us if you think that's a mistake." Shown instead of the form when a pay link names a payment that isn't open. |
Some wording can't be changed here:
- Payments section labels: "Amount Due," "Total," and "Balance:"
- The select placeholder: "Select..." (a field's own
placeholderreplaces it). - Validation messages (except through
errorMessage). - During checkout: "Completing your payment…" and "Something went wrong. Please try again."
- Salesforce errors: an error from Salesforce is shown as written.
features
| Key | Default | What it does |
|---|---|---|
allowBackNavigation | true | Shows the Back button on screens after the first. |
autoFinishCheckout | false | When true, the flow moves on by itself as soon as the payment completes. A brief "Completing your payment…" message shows, and there's 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 toctx.vm.pricingStatefrom a hook. - With no
amount, the total isbaseAmountalone.
- Set the starting figures in
These pricing keys work in summary rows, in showWhen and applyWhen checks, and in hooks:
| Key | Meaning |
|---|---|
baseAmount | The chosen or entered amount. In external mode, the value your code set. |
feeAmount | Written by adjustments. Each one overwrites its target, so the last enabled adjustment aimed at feeAmount wins. |
amount | The total of baseAmount and 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).
| Type | What it shows |
|---|---|
fields | A grid of fields. Set columns: 2 for two columns. |
summary | Read-only rows from the pricing keys. Each row takes key, label, format: 'currency', and emphasis. |
content | A block of text or of html you control. class is optional. |
image | An image set by src and alt. caption and class are optional. |
divider | A horizontal rule. |
alert | A callout with text or html. severity is 'info' (the default), 'success', 'warning', or 'error'. |
payments | The payments of an existing Opportunity. Shows only with a payment model. |
checkout | Stripe's payment form. Goes on the role: 'checkout' screen. |
confirmation | The success image, heading, and description from text. |
custom | A 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.
| Type | Notes |
|---|---|
preset-amount | See below. |
text | A text box. |
email | A text box that checks the address format. |
currency | A number formatted as money. min sets the floor (default 0). |
textarea | Several lines of text. rows defaults to 3. |
select | A dropdown. Pass options: [{label, value}], or name other keys with optionLabel and optionValue. |
radio | A set of choices. Each option can carry a description. |
checkbox | A single checkbox that starts 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.allowCustomadds a box for another amount.defaultSelectedpicks a button to start with. - Copy:
presetLabelsrenames individual buttons. For example,{ "100": "$100 Sustainer" }renames the $100 button.customLabelandcustomPlaceholderset 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=truesets the text"true". A check likeform.coverFees === truewon't match it. - Amounts fill the custom box.
flpaypre_amount=100fills 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. Names Salesforce acts on, such as
priceIdandmode, are included. On a page where those matter, set them inpayload.staticValues. A URL can't override those.
Paying an existing Opportunity. Your page's URL parameters are passed on to the connector. It 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. Later steps win over earlier ones:
- Its
defaultValue(only whenstate.defaultsdoesn't name the field). state.defaults.- A
flpaypre_URL value. testData.
theme
There are two layers, applied in order. The later one wins:
theme.stylesheetUrl: the address of a CSS file.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; }
`
}
| Variable | Default | Used for |
|---|---|---|
--flpay-brand | #0176d3 | Main buttons, links, and selected states |
--flpay-brand-hover | #014486 | The Back button and preset buttons on hover |
--flpay-brand-active | #032d60 | The selected preset button's text |
--flpay-focus | #1b96ff | The focus ring |
--flpay-surface | #ffffff | The card's background |
--flpay-surface-alt | #fafaf9 | Summary rows |
--flpay-border | #c9c9c9 | Borders |
--flpay-border-weak | #dddbda | Light borders |
--flpay-text | #181818 | Text |
--flpay-text-muted | #5c5c5c | Secondary text |
--flpay-text-label | #3e3e3c | Field labels |
--flpay-radius-sm | 4px | Rounding on inputs and buttons |
--flpay-radius-md | 6px | Rounding on cards |
--flpay-shadow-card | 0 2px 3px rgba(0, 0, 0, 0.08) | The card's shadow |
--flpay-shadow-focus | 0 0 0 2px var(--flpay-focus) | The focus ring's shadow |
--flpay-widget-padding | 24px | Space 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 and use !important.
hooks
The widget calls these functions 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) => { /* ... */ }
}
| Hook | When it runs |
|---|---|
beforeValidate | Before each screen is checked. Throw an error to stop the visitor with your own message. |
afterValidate | After a screen passes its checks. |
beforePayloadBuild | On submit, once the payload is built and just before transformPayload. Changing the form here doesn't change what's sent. |
transformPayload | Last, with the payload. Return what to send. If it returns nothing, the payload as it now stands is sent. |
onPaymentResponse | After Salesforce answers and Stripe's form is showing. |
onError | When 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:
| Property | Description |
|---|---|
config | The whole configuration. |
currentScreen | The current screen's id. |
formState | Field values, by name. |
pricingState | A copy of the pricing keys. To change pricing, write to ctx.vm.pricingState. |
customData | A shared place to keep data between hooks, screens, and custom components. |
selectedPresetAmount | The selected preset, if any. |
vm | For advanced cases, the widget's Vue instance. |
payload
The widget sends this to Salesforce on submit, in this order. Later entries win:
- Every field's value. That includes hidden fields and any
flpaypre_values. amount,baseAmount, andfeeAmountfrom pricing (in dollars).- Any payment model selections.
payload.staticValues.- Whatever
transformPayloadreturns.
Leave payload.method as 'createCheckoutSession'. It's the only one the connector answers. With any other name, the form waits forever.
Names Salesforce acts on:
| Name | What it does |
|---|---|
itemName, paymentCurrency | Required unless you send priceId. Name what's being paid for, and set the currency Stripe charges in. |
itemDescription | A description on Stripe's receipt line. |
email, firstName, lastName, companyName | Find or create the payer and their company. |
amount, feeAmount, baseAmount | The payment amount. |
mode, priceId, interval, intervalCount | Start a subscription. See Subscriptions. |
setupFutureUsage | 'off_session' saves the card so Salesforce can charge it later. |
quantity, collectPhoneNumber, customerId, metadata, clientReferenceId, returnUrl | Passed 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 Model | What the visitor sees |
|---|---|
| Opportunity Only | A read-only "Amount Due". |
| Summed Payments | Read-only payment lines and their total. |
| Selective Payments | A checkbox per payment. All start ticked, and the total follows the ticks. |
| Dynamic Payments | An amount box per payment that starts at 0 and is capped at its balance. Salesforce refuses an amount that's 0 or less or more than the balance. |
Summed Payments charges every payment the Opportunity still owes, even if one was added after the page loaded. Once the payment goes through, Salesforce marks each payment it charged as paid.
Salesforce charges only payments the Opportunity still owes. If one is paid, written off, or canceled after the page loads, the visitor sees "One of these payments isn't open anymore. Please refresh the page and try again."
Paying one payment
A pay link pays a single payment record. Add flpaypid=<payment ID> beside flpayoid:
https://yoursite.org/pay?flpayoid=006xx000000abcd&flpaypid=a01xx000000efgh
The payments section shows a read-only "Amount Due" of what's still owed on that payment, whatever the Opportunity's payment model. Salesforce charges that amount and marks the payment paid once the payment goes through.
If the Opportunity doesn't owe that payment, the form doesn't show. That covers a payment that's paid, written off, or canceled and one on another Opportunity. The visitor reads text.paymentNotOpen instead, and the page never charges the whole Opportunity in its place.
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:
| Send | What Stripe charges |
|---|---|
priceId | A recurring Stripe price. It can be one you made on a Product's record page. The price sets the amount, currency, and billing period, so the page's own amount is ignored. |
amount with interval | The form's amount every interval: day, week, month (the default), or year. Add intervalCount for longer periods. For example, month with intervalCount: 3 bills quarterly. Like any payment without a price, it needs itemName and paymentCurrency. |
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
}
Products, invoices, and subscriptions covers what a subscription leaves on your records and how renewals and cancellations work.
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, andconfig. It also receives anything insection.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'smin(1 if that isn't set). Turn it off on pages where the amount comes from a Stripe price or a payment model. - Errors:
- Each error shows under its field. The amount error 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
errorMessagereplaces "This field is required."
A complete example
This donation page has 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. A Flow can read it there.
Public API
| Member | What 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. 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.version | The widget's version (for example, '1.4.0'). |
FLPayments.build | A short fingerprint of the exact file the page loaded. It tells 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). On error, it carries a data-fl-error that says what went wrong.
A page can hold several forms if each has 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
| Address | What it serves |
|---|---|
https://cdn.toflourish.org/payments/v1/payment-widget.js | The 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.js | A minified copy of the same file. |
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.