Add a Hosted Fields example on Next.js and React - #1
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
nextjs/, a third merchant integration of the same payment, next togo-js/andnodejs-express-js/. Next.js 15 (App Router) + React 19 + TypeScript, ported fromgo-js/one to one: same OAuth 1.0a RSA-SHA256 signing, same three gateway calls, same wire contract
on
/payand/status, samecontrolcheck on the 3DS return, same screens.The first two examples answer "which server language". This one answers a different question —
what the integration looks like when the page is React — and that is the only reason to have
it. The card fields are cross-origin iframes the SDK injects imperatively into elements React
also owns, which is exactly the case React handles badly by default.
What is shared and what is not
public/styles.cssis now shared by all three apps, byte for byte, and CI diffs it againstall three. The React example cannot share
checkout.js,status.jsor the two views — theyare ported to components — so those four stay a two-app contract as before. That split is what
keeps the three looking identical without pretending React can consume a static HTML file.
The screens match
docs/checkout.pnganddocs/result.pngto the pixel.The two places Next forced a different shape
Both are documented in
nextjs/README.mdandnextjs/CLAUDE.md, and neither weakens whatgo-jswas protecting.The 3DS return. An App Router page cannot serve a POST, and
page.tsxandroute.tscannot share a path. So
redirect_urlpoints at{prefix}/result/callback, which recomputessha1(status + orderid + merchant_order + MERCHANT_CONTROL), answers403on a mismatch, andotherwise puts the verified order into a short-lived
httpOnlycookie and answers303to{prefix}/result. The identifiers the page polls with still come from the gateway's signedcallback, never from the browser's URL.
BASE_PATHis build-time. It becomesbasePath, whichnext buildbakes into the output.Keeping it in
.envmeans the build and the runtime agree, but changing it means rebuilding.PORTis the other odd one: Next picks the port before it loads.env, so it only works as areal environment variable —
yarn dev/yarn startdefault to 3002 and systemd supplies itthrough
EnvironmentFile. Everything else.envdrives at runtime, standalone included.No
NEXT_PUBLIC_*: the SDK URL, endpoint id, ephemeral ticket and amount reach the browser asprops from the server component, which is the React shape of the
window.CONFIGthe other twoinject. That keeps one runtime-configured artefact instead of baking credentials into a bundle.
React and the SDK
Four rules, each enforced and explained at its site, because each one silently breaks the card
fields if ignored:
classNameis a constant — Reactleaves foreign DOM alone but rewrites an attribute whose rendered value changed, which would
wipe the
hf-field--focus/--filledclasses the SDK sets. The error ring therefore goesthrough
classList, not through a rendered class;HostedFields.init()runs once behind a ref guard, because React 19 invokes effectstwice in development StrictMode — and nothing is destroyed on cleanup for the same reason;
sdk.setStyle()is pushed for all three fields on every theme change, afterdata-themeis on
<html>, since the iframes inherit nothing from the stylesheet.field-style.tsreadsthe values back out of the same
--pay-*custom properties the container uses, so the twosides of the seam cannot drift;
next buildimports the routemodules to collect them, and CI builds without credentials.
Also in this PR
nextjsjob (yarn install --frozen-lockfile,yarn lint,yarn build); theshared frontendjob now diffsstyles.cssacross three apps and the scripts and viewsacross two.
hosted-fields-examples-nextjs.tar.gzfromoutput: 'standalone'—server.js,.next/,public/anddeploy/, runs on plain Node 20 with nonpm install.deploy/: systemd unit, nginx snippet and env template, port 3002, prefix/hosted-fields-examples-nextjs.README.mdandCLAUDE.mdupdated for the third example;nextjs/README.mdandnextjs/CLAUDE.mdwritten.Verification
Run against the sandbox, not just built:
4444 4444 4444 4448— tokenize, Sale, status polling, 3DS, approved,with
Cardholder ANNA WEBERon the result panel, which is the merchant's own input betweenthe gateway's iframes reaching the Sale as
card_printed_name;sdk.setStyle), cardholder auto-fill andupper-casing with caret preservation, per-field validation firing before the ticket is spent;
controlon the callback answers403; a bareGET {prefix}/resultshows the"nothing to show" state; the standalone artefact runs and serves
styles.cssunder the prefix;yarn lintandyarn buildclean.One gap: the local
.envhas no realMERCHANT_CONTROL, so the genuine cross-site POST fromthe gateway was answered
403(signature computed against a placeholder). The callback route,the signature check, the cookie, the
303and the polling on/resultwere all verified witha same-site POST carrying a correctly computed
control. What remains unproven end to end isonly the browser accepting
Set-Cookieon a cross-site POST — standard behaviour, sinceSameSite governs sending and the
303makes the follow-up a top-level GET, whichLaxallows.Worth one run with the real secret before tagging a release.