Payment widget
How to put a Flourish Payments form on your own website.
Version 1.3.0 | A configuration-driven payment widget built on Vue 3, PrimeVue and Stripe.js
The widget renders a multi-step payment form on any page. You describe the screens, fields, pricing and copy in a single configuration object; there is no framework code to write. What it collects goes to Salesforce through the public connector, and what happens to the record afterwards is on Payments in Salesforce.
Before any of this works, an admin has to have finished Setup.
Quick Start
1. Include the Dependencies
The widget loads none of its own dependencies, so the page has to supply 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/umd/primevue.min.js"></script>
<script src="https://unpkg.com/@primeuix/themes/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 is no stylesheet to link. The widget carries its own CSS.
If one of the four is missing or fails to load, the widget says so instead of rendering: the visitor gets one plain sentence, and the browser console names each missing library and the URL that supplies it. create() returns null in that case rather than throwing.
2. Add a Mount Point
<div id="app"></div>
3. Initialize the widget
<script>
FLPayments.create('#app', {
screens: [
{
id: 'donate',
sections: [
{
fields: [
{
name: 'amount',
type: 'preset-amount',
label: 'Choose an amount',
presets: [25, 50, 100, 250],
allowCustom: true,
customLabel: 'Or enter a custom amount',
required: true
}
]
}
]
},
{
id: 'review',
role: 'submit',
sections: [
{
type: 'summary',
rows: [
{ key: 'totalAmount', label: 'Total', format: 'currency', emphasis: true }
]
}
]
},
{
id: 'pay',
role: 'checkout',
sections: [{ type: 'checkout' }]
},
{
id: 'done',
sections: [{ type: 'confirmation' }]
}
],
text: {
title: 'Make a Donation',
description: 'Your support makes a difference.'
}
});
</script>
Configuration Reference
All options are passed as the second argument to FLPayments.create(selector, config).
screens
An array of screen objects that define the multi-step flow. Each screen has:
| Property | Type | Description |
|---|---|---|
id | string | Required. Unique identifier for the screen. |
role | string | Optional. Controls screen behavior. See Screen Roles. |
sections | array | Array of section objects displayed on this screen. |
Screen Roles
| Role | Behavior |
|---|---|
| (none) | Default. Validates visible fields, then advances to the next screen. |
submit | Validates, builds the checkout payload, and sends it to the payments connector (Salesforce). |
checkout | Mounts the Stripe Embedded Checkout using the client secret returned from the submit step. |
The last screen in the array is treated as the final screen (no Next button is shown).
Sections
Each section is an object inside a screen's sections array. The type property determines how the section renders.
fields (default)
Renders a form with input fields. If type is omitted and a fields array is present, this is the default.
{
label: 'Contact Information', // optional heading
description: 'We need this...', // optional description text
columns: 2, // optional: 2-column grid layout
fields: [ /* field objects */ ],
showWhen: function (formState) { return formState.type === 'individual'; } // optional
}
summary
Displays a read-only summary table of pricing values.
{
type: 'summary',
rows: [
{ key: 'baseAmount', label: 'Subtotal', format: 'currency' },
{ key: 'feeAmount', label: 'Processing Fee', format: 'currency' },
{ key: 'totalAmount', label: 'Total', format: 'currency', emphasis: true }
]
}
Each key maps to a property in the computed pricing state.
content
Renders plain text or raw HTML.
{ type: 'content', text: 'Thank you for your generosity.' }
// or
{ type: 'content', html: '<p>Visit <a href="...">our site</a> for more info.</p>', class: 'my-class' }
image
Displays an image with an optional caption.
{ type: 'image', src: 'https://example.com/photo.jpg', alt: 'Photo', caption: 'Our team' }
divider
Renders a horizontal rule.
{ type: 'divider' }
alert
Displays a callout/alert box.
{ type: 'alert', text: 'Payments are non-refundable.', severity: 'warn' }
// severity: 'info' (default), 'warn', 'error'
// Can also use `html` instead of `text`.
checkout
Mounts the Stripe Embedded Checkout element. Use on a screen with role: 'checkout'.
{ type: 'checkout' }
confirmation
Shows a success message after payment completes. Displays the configured confirmation image, heading, and description.
{ type: 'confirmation' }
payments
Renders payment line items received from the Salesforce connector. The display adapts automatically based on the paymentModel returned by the connector. See Payment Models below, and Payment models for how an Opportunity is set up for one.
{ type: 'payments' }
custom
Renders a custom Vue component registered via FLPayments.registerComponent().
{ type: 'custom', component: 'donation-thermometer', props: { color: 'blue' } }
Conditional Sections
Any section can include a showWhen function to control visibility:
{
type: 'alert',
text: 'A fee will be added.',
severity: 'info',
showWhen: function (formState) { return formState.coverFee === true; }
}
Field Types
Fields are defined inside a fields section. Every field requires a name (string) that maps to a key in the form state.
Common Field Properties
| Property | Type | Description |
|---|---|---|
name | string | Required. Key in the form state. |
label | string | Display label above the field. |
placeholder | string | Placeholder text. |
required | boolean or function | Whether the field is required. Can be a function: function(formState, pricingState) { ... } |
defaultValue | any | Initial value for the field. |
errorMessage | string | Custom validation error message. |
showWhen | function | Conditional visibility: function(formState, pricingState) { return true/false; } |
span | number | Set to 2 to span full width in a 2-column grid. |
preset-amount
Preset amount buttons with an optional custom input.
{
name: 'amount',
type: 'preset-amount',
label: 'Select Amount',
presets: [25, 50, 100, 250],
presetLabels: { '25': 'Student - $25' }, // optional custom button labels
defaultSelected: 50, // optional default selection
allowCustom: true, // show a custom amount input
customLabel: 'Custom Amount',
customPlaceholder: 'Enter amount',
min: 1, // minimum custom amount
required: true
}
text
A standard text input.
{ name: 'fullName', type: 'text', label: 'Full Name', required: true }
email
A text input with email format validation.
{ name: 'email', type: 'email', label: 'Email Address', required: true }
currency
A formatted currency input.
{ name: 'otherAmount', type: 'currency', label: 'Amount', min: 0 }
textarea
A multi-line text area.
{ name: 'notes', type: 'textarea', label: 'Notes', rows: 4, placeholder: 'Optional...' }
select
A dropdown select.
{
name: 'fund',
type: 'select',
label: 'Designation',
placeholder: 'Select a fund...',
options: [
{ label: 'General Fund', value: 'general' },
{ label: 'Scholarship', value: 'scholarship' }
],
optionLabel: 'label', // default: 'label'
optionValue: 'value', // default: 'value'
required: true
}
radio
A radio button group with optional descriptions.
{
name: 'frequency',
type: 'radio',
label: 'Giving Frequency',
options: [
{ label: 'One-Time', value: 'once', description: 'A single gift' },
{ label: 'Monthly', value: 'monthly', description: 'Recurring each month' }
],
required: true
}
checkbox
A single checkbox (boolean toggle).
{ name: 'coverFee', type: 'checkbox', checkboxLabel: 'I\'d like to cover the processing fee' }
text
Customize all user-facing labels and messages.
text: {
title: 'Make a Payment',
description: 'Complete the form below.',
paymentDescription: 'Enter your card details.',
confirmationImage: 'https://example.com/success.png',
confirmationHeading: 'Payment Complete',
confirmationDescription: 'Thank you for your payment.',
nextButton: 'Continue',
finishButton: 'Submit Payment',
backButton: 'Go Back',
connectorLoading: 'Loading secure payment system...',
connectorTimeout: 'Payment system is unavailable. Please try again later.',
connectorMissingSettings: 'Payment system is not configured. Please contact the administrator.'
}
All properties are optional and have sensible defaults.
pricing
Controls how the payment amount is calculated.
pricing: {
mode: 'singleAmount', // 'singleAmount' or 'external'
currency: 'usd', // ISO 4217 currency code
amountFieldName: 'amount', // which form field holds the base amount
adjustments: [
{
enabled: true,
type: 'percentage',
baseKey: 'baseAmount', // pricing state key to calculate from
targetKey: 'feeAmount', // pricing state key to write the result to
percent: 0.03, // 3% fee
applyWhen: function (formState) { return formState.coverFee === true; }
}
]
}
Pricing State
The widget computes the following pricing state values automatically (in singleAmount mode):
| Key | Description |
|---|---|
baseAmount | The selected or entered amount. |
subtotal | Same as baseAmount before adjustments. |
feeAmount | Calculated fee (from adjustments). |
totalAmount | baseAmount + all adjustment totals. |
In external mode, pricing values are managed externally via pricingDefaults or hooks.
connector
Settings for the Salesforce payments connector (LWC iframe).
connector: {
timeoutMs: 10000, // milliseconds to wait for the connector to respond
disabled: false // set true to skip the connector (for testing)
}
payload
Controls how the checkout payload is sent to the backend.
payload: {
method: 'createCheckoutSession', // the Apex method name to invoke
staticValues: { // merged into every payload (overrides form values)
campaignId: '701xx000000abcd',
recordTypeId: '012xx000000efgh'
}
}
The payload is built by merging: form fields + pricing values (converted to cents) + payment selections + staticValues.
state
Set initial values for the form and pricing state.
state: {
defaults: { // initial form field values
frequency: 'once',
coverFee: false
},
pricingDefaults: { // initial pricing state (for 'external' mode)
baseAmount: 0,
totalAmount: 0
}
}
stripe
Stripe-specific settings.
stripe: {
mountSelector: '#fl-checkout' // CSS selector for the Stripe checkout mount point
}
The publishableKey is provided automatically by the connector; you do not need to set it manually.
features
Toggle widget behaviors.
features: {
allowBackNavigation: true, // show/hide the Back button
autoFinishCheckout: true // advance to confirmation as soon as Stripe reports success
}
autoFinishCheckout removes the manual Finish step. When Stripe reports the payment complete, the widget shows a brief "Completing your payment..." state, tears the Stripe embed down, and moves straight to the confirmation screen. Leave it off and the payer clicks Finish themselves.
validation
validation: {
requireAmount: true // require a non-zero amount before proceeding
}
theme
Customize the widget's appearance.
theme: {
stylesheetUrl: 'https://example.com/my-theme.css', // external stylesheet (overrides defaults)
css: ':host { --flpay-brand: #e63946; }' // inline CSS (highest priority)
}
Available CSS Variables
| Variable | Default | Description |
|---|---|---|
--flpay-brand | #0176d3 | Primary brand color (buttons, links) |
--flpay-brand-hover | #014486 | Button hover color |
--flpay-brand-active | #032d60 | Button active/pressed color |
--flpay-surface | #ffffff | Card background |
--flpay-border | #c9c9c9 | Border color |
--flpay-text | #181818 | Primary text color |
--flpay-text-muted | #5c5c5c | Secondary/muted text |
--flpay-radius-md | 6px | Card border radius |
The widget renders inside a Shadow DOM, so your page's CSS will not leak in. Use the theme config to apply custom styles.
hooks
Lifecycle hooks let you run custom logic at key moments in the flow.
hooks: {
beforeValidate: function (context) {
// Runs before field validation. `context` contains formState, pricingState, config, etc.
},
afterValidate: function (context) {
// Runs after validation passes.
},
beforePayloadBuild: function (context) {
// Runs before the payload is assembled.
},
transformPayload: function (payload, context) {
// Modify the payload before it's sent. Return the modified payload.
payload.customField = 'value';
return payload;
},
onPaymentResponse: function (response, context) {
// Runs after a successful payment response from the backend.
},
onError: function (error, context) {
// Runs when an error occurs.
}
}
Hook Context Object
Every hook receives a context object with:
| Property | Description |
|---|---|
config | The full widget configuration. |
currentScreen | ID of the active screen. |
formState | Current form field values. |
pricingState | Computed pricing values. |
customData | A shared object for storing arbitrary data across hooks. |
selectedPresetAmount | The currently selected preset amount (if any). |
vm | The Vue component instance (advanced use). |
paymentsLwc
Advanced. How the widget reaches Salesforce. siteUrl is the connector page your admin set up, the same URL they put in Public Connector URL. The widget only accepts messages from that exact origin, so a value that does not match the page it frames breaks the handshake rather than loosening it.
paymentsLwc: {
siteUrl: 'https://your-salesforce-site.my.site.com/payments',
iframeId: 'lwc-iframe',
wrapperSelector: '.lwc-iframe-wrapper'
}
Payment Models
When the connector returns a paymentModel, that determines how amounts are displayed. The payments section type adapts automatically.
| Model | Behavior |
|---|---|
| Opportunity Only | Displays a single read-only amount from the Opportunity record. |
| Summed Payments | Lists all payment line items as read-only rows with a calculated total. |
| Selective Payments | Checkbox list. The user selects which payments to include. All are pre-selected by default. |
| Dynamic Payments | Editable amount inputs. The user can enter a partial payment for each line item (up to the balance remaining). |
URL Parameters
Pre-fill Form Fields
Form fields can be pre-populated from URL query parameters using the flpaypre_ prefix.
https://example.com/donate?flpaypre_email=jane@example.com&flpaypre_fullName=Jane+Doe
This sets formState.email to jane@example.com and formState.fullName to Jane Doe.
Payment Context Parameters
These parameters are read by the Salesforce connector LWC to load an existing Opportunity:
| Parameter | Description |
|---|---|
flpayoid | Opportunity ID. Loads the Opportunity's payment model, amount, and child payments. |
flpaypid | Payment record ID. Tracks the Stripe session against a specific NPSP/NPC payment record. |
See Paying an existing Opportunity for what the connector does with them.
Custom Components
Register custom Vue components that can be used in custom sections:
FLPayments.registerComponent('donation-thermometer', {
props: ['section', 'formState', 'pricingState', 'customData', 'config'],
template: '<div class="thermometer">{{ pricingState.totalAmount }}</div>'
});
FLPayments.create('#app', {
screens: [{
id: 'main',
sections: [
{ type: 'custom', component: 'donation-thermometer' }
]
}]
});
Custom components receive these props automatically: section, formState, pricingState, customData, config, plus any additional props defined in section.props.
What happens in Salesforce
The Salesforce half of this is documented separately, because an admin owns it rather than the page author:
- Setup covers the Stripe keys, the public connector site and the webhook.
- Payments in Salesforce covers the records a checkout creates, the payment models, server-side pricing, and what the form collects that lands in Payment Custom Data.
- Flow actions covers charging a saved card later.
Two things are worth knowing while you build the page. The amount the widget displays is not the amount that gets charged: Salesforce recomputes it before Stripe is called, so pricing you set here is a preview. And any field you add that the package does not recognize is captured anyway, with no configuration at either end.
Complete Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Donation Form</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/primevue/umd/primevue.min.js"></script>
<script src="https://unpkg.com/@primeuix/themes/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="app"></div>
<script>
FLPayments.create('#app', {
text: {
title: 'Support Our Mission',
description: 'Choose an amount and complete your donation.',
finishButton: 'Proceed to Payment',
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,
required: true
}
]
},
{
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: 'totalAmount', 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',
baseKey: 'baseAmount',
targetKey: 'feeAmount',
percent: 0.03,
applyWhen: function (formState) { return formState.coverFee === true; }
}
]
},
payload: {
method: 'createCheckoutSession',
staticValues: {
campaignId: '701xx000000abcd'
}
},
theme: {
css: ':host { --flpay-brand: #2d6a4f; }'
}
});
</script>
</body>
</html>
API
| Method | Description |
|---|---|
FLPayments.create(selector, config) | Mount the widget into the given DOM element. Returns the Vue app instance. |
FLPayments.registerComponent(name, definition) | Register a custom Vue component for use in custom sections. Must be called before create(). |
FLPayments.version | The current widget version string. |