Framework Integrations
Fluid widgets are standard web components and are compatible with any framework. Every integration follows the same rules:
- Load the Fluid script once, after the user authenticates, and never on every render
- Pass the real, current
session-idof the authenticated user and keep it stable. See Session Handling - Attach event listeners synchronously, before or alongside mounting, so you don’t miss the
initialisedevent - Open the cashier from a user action once
initialisedhas fired, and setopenback tofalseon theclosecommand - Remove the widget when the session ends
Object-valued attributes (user-data, bonuses, transaction-attributes, transaction-limits) are passed as stringified JSON in every framework. See Data Objects for their shapes.
Loading the script
Section titled “Loading the script”Inject the script once per page, for example from your authenticated layout, and guard it so a re-render doesn’t add it twice:
let fluidScriptLoaded = false;
export function loadFluidScript() { if (fluidScriptLoaded) return; fluidScriptLoaded = true;
const script = document.createElement('script'); script.src = 'https://get.fluidpayments.io/index.js'; script.onload = () => { // Register window.fluid.bridge handlers here if you use the Bridge API }; document.head.appendChild(script);}React sets unknown attributes on custom elements as strings, which suits the Fluid attributes. Attach the listeners with useLayoutEffect, which runs before the widget initialises; useEffect can run too late and miss the initialised event.
import { useLayoutEffect, useRef, useState } from 'react';
function FluidCashier({ operatorId, sessionId, userId, locale, currency, country }) { const widgetRef = useRef(null); const [ready, setReady] = useState(false);
useLayoutEffect(() => { const widget = widgetRef.current;
const handleInfo = (e) => { if (e.detail.message === 'initialised') { setReady(true); } }; const handleError = (e) => console.error('Fluid error:', e.detail); const handleCommand = (e) => { if (e.detail.message === 'close') { widget.setAttribute('open', 'false'); } };
widget.addEventListener('fluid-info', handleInfo); widget.addEventListener('fluid-error', handleError); widget.addEventListener('fluid-command', handleCommand);
return () => { widget.removeEventListener('fluid-info', handleInfo); widget.removeEventListener('fluid-error', handleError); widget.removeEventListener('fluid-command', handleCommand); }; }, []);
return ( <> <button disabled={!ready} onClick={() => widgetRef.current.setAttribute('open', 'true')}> Deposit </button> <fluid-widget ref={widgetRef} operator-id={operatorId} session-id={sessionId} user-id={userId} locale={locale} currency={currency} country={country} /> </> );}A complete working example is available at github.com/soltechno/fluid-react-integration (live at fluid-react-integration.vercel.app).
Angular
Section titled “Angular”import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, Input, OnDestroy, OnInit, ViewChild,} from '@angular/core';
@Component({ selector: 'app-fluid-cashier', standalone: true, template: ` <button [disabled]="!ready" (click)="open()">Deposit</button> <fluid-widget #fluidWidget [attr.operator-id]="operatorId" [attr.session-id]="sessionId" [attr.user-id]="userId" [attr.locale]="locale" [attr.currency]="currency" [attr.country]="country" ></fluid-widget> `, // Lets Angular accept the <fluid-widget> custom element schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class FluidCashierComponent implements OnInit, OnDestroy { @ViewChild('fluidWidget', { static: true }) widgetRef!: ElementRef; @Input() operatorId!: string; @Input() sessionId!: string; @Input() userId!: string; @Input() locale!: string; @Input() currency!: string; @Input() country!: string;
ready = false;
ngOnInit() { const widget = this.widgetRef.nativeElement; widget.addEventListener('fluid-info', this.handleInfo); widget.addEventListener('fluid-error', this.handleError); widget.addEventListener('fluid-command', this.handleCommand); }
ngOnDestroy() { const widget = this.widgetRef.nativeElement; widget.removeEventListener('fluid-info', this.handleInfo); widget.removeEventListener('fluid-error', this.handleError); widget.removeEventListener('fluid-command', this.handleCommand); }
open() { this.widgetRef.nativeElement.setAttribute('open', 'true'); }
handleInfo = (e: CustomEvent) => { if (e.detail.message === 'initialised') { this.ready = true; } };
handleError = (e: CustomEvent) => console.error('Fluid error:', e.detail);
handleCommand = (e: CustomEvent) => { if (e.detail.message === 'close') { this.widgetRef.nativeElement.setAttribute('open', 'false'); } };}A complete working example is available at github.com/soltechno/fluid-angular (live at demo.fluidpayments.io).
Configure Vue to treat fluid- elements as custom elements, otherwise it tries to resolve <fluid-widget> as a Vue component. With Vite:
import vue from '@vitejs/plugin-vue';
export default { plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('fluid-'), }, }, }), ],};<template> <button :disabled="!ready" @click="open">Deposit</button> <fluid-widget ref="fluidWidget" :operator-id="operatorId" :session-id="sessionId" :user-id="userId" :locale="locale" :currency="currency" :country="country" /></template>
<script setup>import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps({ operatorId: String, sessionId: String, userId: String, locale: String, currency: String, country: String,});
const fluidWidget = ref(null);const ready = ref(false);
function open() { fluidWidget.value.setAttribute('open', 'true');}
function handleInfo(e) { if (e.detail.message === 'initialised') { ready.value = true; }}
function handleError(e) { console.error('Fluid error:', e.detail);}
function handleCommand(e) { if (e.detail.message === 'close') { fluidWidget.value.setAttribute('open', 'false'); }}
onMounted(() => { const widget = fluidWidget.value; widget.addEventListener('fluid-info', handleInfo); widget.addEventListener('fluid-error', handleError); widget.addEventListener('fluid-command', handleCommand);});
onUnmounted(() => { const widget = fluidWidget.value; widget.removeEventListener('fluid-info', handleInfo); widget.removeEventListener('fluid-error', handleError); widget.removeEventListener('fluid-command', handleCommand);});</script>Svelte
Section titled “Svelte”Svelte works with custom elements out of the box. Load the script once when the user authenticates, for example by calling loadFluidScript() (see Loading the script) in the onMount of your authenticated layout.
<script> import { onMount, onDestroy } from 'svelte';
export let operatorId; export let sessionId; export let userId; export let locale; export let currency; export let country;
let widgetEl; let ready = false;
function open() { widgetEl.setAttribute('open', 'true'); }
function handleInfo(e) { if (e.detail.message === 'initialised') { ready = true; } }
function handleError(e) { console.error('Fluid error:', e.detail); }
function handleCommand(e) { if (e.detail.message === 'close') { widgetEl.setAttribute('open', 'false'); } }
onMount(() => { widgetEl.addEventListener('fluid-info', handleInfo); widgetEl.addEventListener('fluid-error', handleError); widgetEl.addEventListener('fluid-command', handleCommand); });
onDestroy(() => { widgetEl.removeEventListener('fluid-info', handleInfo); widgetEl.removeEventListener('fluid-error', handleError); widgetEl.removeEventListener('fluid-command', handleCommand); });</script>
<button disabled={!ready} on:click={open}>Deposit</button><fluid-widget bind:this={widgetEl} operator-id={operatorId} session-id={sessionId} user-id={userId} {locale} {currency} {country}/>Custom elements and TypeScript
Section titled “Custom elements and TypeScript”If you use TypeScript, you may need to declare the custom elements to avoid type errors:
declare global { namespace JSX { interface IntrinsicElements { 'fluid-widget': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { 'operator-id'?: string; 'session-id'?: string; 'user-id'?: string; locale?: string; currency?: string; country?: string; open?: string; }; } }}