Skip to content

Framework Integrations

Fluid widgets are standard web components and are compatible with any framework. Every integration follows the same rules:

  1. Load the Fluid script once, after the user authenticates, and never on every render
  2. Pass the real, current session-id of the authenticated user and keep it stable. See Session Handling
  3. Attach event listeners synchronously, before or alongside mounting, so you don’t miss the initialised event
  4. Open the cashier from a user action once initialised has fired, and set open back to false on the close command
  5. 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.

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).

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:

vite.config.js
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 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}
/>

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;
};
}
}
}