Decoupled SPA (Vue)
The package is framework-agnostic. The React example backend and flow are unchanged for any SPA; only the view layer differs. The contract is plain HTTP plus a full-page form submit:
POST /sisp/payment/intentreturns{ action, fields, ref }.- Build a
<form>and submit it full-page to the gateway. - After payment the package redirects to
${frontendResultUrl}?ref=.... - The result route reads
refand pollsGET /sisp/transactions/:ref.
The gateway submit is plain DOM, identical everywhere:
const API = 'http://localhost:3000';
export async function startPayment(data: Record<string, string>) {
const response = await fetch(`${API}/sisp/payment/intent`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
...data,
items: [
{ product_name: 'Plano Pro', quantity: '1', unit_price: data.amount, total_price: data.amount },
],
}),
});
const { action, fields } = await response.json();
const form = document.createElement('form');
form.method = 'POST';
form.action = action;
for (const [name, value] of Object.entries(fields)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = String(value);
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}
Checkout
<script setup lang="ts">
import { ref } from 'vue';
import { startPayment } from './payment';
const submitting = ref(false);
async function pay(event: Event) {
submitting.value = true;
const form = event.target as HTMLFormElement;
await startPayment(Object.fromEntries(new FormData(form).entries()) as Record<string, string>);
}
</script>
<template>
<form @submit.prevent="pay">
<input name="amount" type="number" value="1500" required />
<input name="customer_email" type="email" value="cliente@example.cv" required />
<input name="customer_country" value="CV" required />
<input name="customer_city" value="Praia" required />
<input name="customer_address" value="Av. Cidade de Lisboa" required />
<input name="customer_postal_code" value="7600" required />
<button :disabled="submitting">Pay</button>
</form>
</template>
Result route
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const API = 'http://localhost:3000';
const transaction = ref<{ status: string; amount: number; detail: string | null } | null>(null);
onMounted(() => {
const reference = new URLSearchParams(location.search).get('ref');
if (!reference) return;
let attempts = 0;
const poll = async () => {
attempts += 1;
const response = await fetch(`${API}/sisp/transactions/${reference}`);
if (!response.ok) return;
transaction.value = await response.json();
if (transaction.value?.status === 'pending' && attempts < 10) {
setTimeout(poll, 1000);
}
};
poll();
});
</script>
<template>
<p v-if="transaction">Status: {{ transaction.status }} ({{ transaction.amount }})</p>
<p v-else>Loading...</p>
</template>
The backend (API + CORS + frontendResultUrl) is exactly the one from the React example, on either knex or Prisma storage. Only the SPA router and components change per framework.
Next: Decoupled SPA (Svelte)