@payfanout/adapter-paysafe
v0.4.3
Published
Client-side Paysafe adapter for PayFanout (Paysafe.js hosted iframe fields, tokenize-first). No secrets, no server code.
Readme
@payfanout/adapter-paysafe
Client-side Paysafe adapter for PayFanout: Paysafe.js hosted iframe fields, tokenize-first, rendered embedded in your UI.
No secrets, no server code. This package holds only a browser-safe public key.
It implements the ClientPaymentAdapter contract from @payfanout/core and plugs into
@payfanout/react, which renders the fields and drives the pay flow. Paysafe.js is loaded
lazily via a <script> tag only when this adapter is actually mounted, there is nothing
extra to install and no SDK download during SSR.
📖 Documentation: https://donapulse.github.io/payfanout/ · React usage · Set up Paysafe
Installation
pnpm add @payfanout/react @payfanout/adapter-paysafe react react-domUsage
import { PayFanoutProvider, PaymentFields, PayButton } from "@payfanout/react";
import { PaysafeClientAdapter } from "@payfanout/adapter-paysafe";
const paysafe = new PaysafeClientAdapter({
apiKey: "base64-public-key", // public single-use-token key
environment: "sandbox", // never inferred
});
<PayFanoutProvider adapters={[paysafe]} initialPsp="paysafe">
<PaymentFields clientSecret={session.clientSecret} appearance={designTokens} />
<PayButton
onResult={(result) => …}
onServerCompletion={(clientToken) => postToMyApi("/api/complete", { clientToken })}
>
Pay
</PayButton>
</PayFanoutProvider>This is a tokenize-first PSP: the client tokenizes first, then your server finalizes via
completePayment (called through the onServerCompletion route). <PayButton> branches
into that path automatically, so the UI code is identical to a confirm-on-client PSP.
Split-field layouts are yours
Paysafe uses separate iframe fields (card number, expiry, CVV). Own the layout with named slots, any grid, rows, and labels:
<PaymentFields clientSecret={secret} fieldOptions={{ fields: { cardNumber: { placeholder: "Card number" } } }}>
<div className="my-grid">
<div data-payfanout-field="cardNumber" />
<div className="row">
<div data-payfanout-field="expiryDate" />
<div data-payfanout-field="cvv" />
</div>
</div>
</PaymentFields>Setup and tokenize
- Setup takes the currency and, when the session names a numeric one, the merchant
account (Paysafe.js
accounts.default) from the signed session.fieldOptionscannot override the currency, and the session's account replaces anyfieldOptions.accounts. Paysafe.jsshow()runs right after setup, as Paysafe documents. - Each card tokenize sends a fresh
merchantRefNum, which Paysafe.js requires: the sessionidwhen set, then a random suffix, 255 characters at most. A card retried after a decline never reuses one.
Content-Security-Policy
On CSP-enforcing pages, allow every host Paysafe.js touches, or the hosted fields
fail quietly: the script (hosted.paysafe.com), the card-field iframes
(hosted.paysafe.com in live, hosted.test.paysafe.com in sandbox), and its
parent-page XHRs (telemetry to the hosted hosts, config and BIN lookups to
api.paysafe.com / api.test.paysafe.com):
script-src https://hosted.paysafe.com
frame-src https://hosted.paysafe.com https://hosted.test.paysafe.com
connect-src https://hosted.paysafe.com https://hosted.test.paysafe.com
https://api.paysafe.com https://api.test.paysafe.comThe .test hosts are used only by environment: "sandbox". On a nonce-based
policy, pass the nonce as cspNonce and the adapter puts it on the Paysafe.js
<script> it injects. See
Set up Paysafe for the
per-directive failure modes and what Paysafe.js still needs beyond the nonce.
What's inside
PaysafeClientAdapter, mounts the hosted fields, tokenizes, and resolves the tokenize-first completion.decodeSessionPayloadand the structuralPaysafeJsLike/PaysafeFieldsInstanceLiketypes so the adapter is testable without the real SDK.
Pair it on the server with @payfanout/adapter-paysafe-server,
which issues the signed session context this adapter consumes.
Documentation
License
MIT
