Skip to main content

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:

PropertyTypeDescription
idstringRequired. Unique identifier for the screen.
rolestringOptional. Controls screen behavior. See Screen Roles.
sectionsarrayArray of section objects displayed on this screen.

Screen Roles

RoleBehavior
(none)Default. Validates visible fields, then advances to the next screen.
submitValidates, builds the checkout payload, and sends it to the payments connector (Salesforce).
checkoutMounts 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

PropertyTypeDescription
namestringRequired. Key in the form state.
labelstringDisplay label above the field.
placeholderstringPlaceholder text.
requiredboolean or functionWhether the field is required. Can be a function: function(formState, pricingState) { ... }
defaultValueanyInitial value for the field.
errorMessagestringCustom validation error message.
showWhenfunctionConditional visibility: function(formState, pricingState) { return true/false; }
spannumberSet 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):

KeyDescription
baseAmountThe selected or entered amount.
subtotalSame as baseAmount before adjustments.
feeAmountCalculated fee (from adjustments).
totalAmountbaseAmount + 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

VariableDefaultDescription
--flpay-brand#0176d3Primary brand color (buttons, links)
--flpay-brand-hover#014486Button hover color
--flpay-brand-active#032d60Button active/pressed color
--flpay-surface#ffffffCard background
--flpay-border#c9c9c9Border color
--flpay-text#181818Primary text color
--flpay-text-muted#5c5c5cSecondary/muted text
--flpay-radius-md6pxCard 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:

PropertyDescription
configThe full widget configuration.
currentScreenID of the active screen.
formStateCurrent form field values.
pricingStateComputed pricing values.
customDataA shared object for storing arbitrary data across hooks.
selectedPresetAmountThe currently selected preset amount (if any).
vmThe 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.

ModelBehavior
Opportunity OnlyDisplays a single read-only amount from the Opportunity record.
Summed PaymentsLists all payment line items as read-only rows with a calculated total.
Selective PaymentsCheckbox list. The user selects which payments to include. All are pre-selected by default.
Dynamic PaymentsEditable 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:

ParameterDescription
flpayoidOpportunity ID. Loads the Opportunity's payment model, amount, and child payments.
flpaypidPayment 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

MethodDescription
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.versionThe current widget version string.