@n.exchange/widget
v0.2.1
Published
Embeddable n.exchange widget for partner sites - pair/amount selection through order completion, attributed via a widget-scoped API key and referral code.
Maintainers
Readme
@n.exchange/widget
Embeddable n.exchange exchange widget for partner sites. Handles pair/amount
selection through order completion inside an iframe pointed at
https://n.exchange/embed, attributed to the partner via a widget-scoped
API key and referral code (see the partner dashboard's "Widget" tab to
create one).
Widget keys are deliberately restricted server-side: they can create and look up orders, but never list order history or be used with a referral code other than the one they were locked to at creation. A leaked key's blast radius is bounded to that.
Prerequisites
You need a widget-scoped API key and the referral code it's locked to. Get both from your n.exchange account:
- Log in at n.exchange with a business account.
- Go to Account → Widget (
/user/profile, then the "Widget" tab in the sidebar - business accounts only). - Pick one of your referral codes and create a widget key for it. The raw key is shown once - copy it immediately, it can't be viewed again (only re-created).
Keep the key and code together - the key only ever works with the one referral code it was created for; using a different code returns a 400.
Install
npm install @n.exchange/widgetQuick start (React)
Full working example - a page with nothing else on it:
import { NexchangeWidget } from '@n.exchange/widget/react'
export default function ExchangePage() {
return (
<NexchangeWidget
apiKey={process.env.NEXT_PUBLIC_NEXCHANGE_API_KEY!}
referralCode={process.env.NEXT_PUBLIC_NEXCHANGE_REFERRAL_CODE!}
onReady={() => console.log('widget loaded')}
onOrderCreated={orderId => console.log('order created:', orderId)}
onStatusChanged={(orderId, status) => console.log(orderId, 'is now', status)}
onError={message => console.error('widget error:', message)}
style={{ width: '100%', maxWidth: 480 }}
/>
)
}That's the whole integration - no extra setup, no API calls of your own to
wire up. The component fills its container width and reports its own
height, so just constrain the width with style/className as shown above.
Quick start (plain HTML / any other framework)
Full working example - save as an .html file and open it directly:
<!doctype html>
<html>
<body>
<nexchange-widget
api-key="YOUR_WIDGET_API_KEY"
referral-code="YOUR_REFERRAL_CODE"
style="display: block; max-width: 480px"
></nexchange-widget>
<script type="module" src="https://unpkg.com/@n.exchange/widget"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const widget = document.querySelector('nexchange-widget')
widget.addEventListener('ready', () => console.log('widget loaded'))
widget.addEventListener('order-created', e =>
console.log('order created:', e.detail.orderId),
)
widget.addEventListener('status-changed', e =>
console.log(e.detail.orderId, 'is now', e.detail.status),
)
widget.addEventListener('error', e => console.error('widget error:', e.detail.message))
})
</script>
</body>
</html><nexchange-widget> is a real custom element - it works with any
framework (Vue, Svelte, Angular, plain HTML) exactly the same way, since
customElements.define registers it globally once the script loads.
Props / attributes
| React prop | HTML attribute | Required | Description |
| ---------------- | ----------------- | -------- | ------------------------------------------------ |
| apiKey | api-key | yes | Widget-scoped API key, from the partner dashboard |
| referralCode | referral-code | yes | The referral code the key is locked to |
| address | address | no | Pre-fill the withdraw address |
| baseUrl | base-url | no | Override the embed origin (defaults to https://n.exchange) |
| showSupportChat| show-support-chat | no | Show n.exchange's own support chat bubble inside the widget (defaults to false - hidden unless you opt in) |
| onReady | ready event | no | The widget finished loading |
| onOrderCreated | order-created | no | (orderId: string) |
| onStatusChanged| status-changed | no | (orderId: string, status: string) |
| onError | error event | no | (message: string) |
status is one of the literal strings n.exchange's order model uses -
note that some are multi-word with a space or hyphen, not SNAKE_CASE:
INITIAL, UNCONFIRMED PAYMENT, PRE-RELEASE, PAID, RELEASED,
COMPLETED, CANCELED, INITIATED REFUND, REFUNDED, REFUND FAILED,
PENDING KYC. If you're switching on this value in your own UI, match
these exactly.
Troubleshooting
- Widget shows an error immediately on load - the API key and referral code don't match (a widget key only works with the one referral code it was locked to), the key was deactivated, or it was mistyped/truncated when copied. Re-check both values in the dashboard's Widget tab.
- Nothing renders / blank space where the widget should be - open the
browser console. If you see a CSP (
frame-src) violation, your page's Content-Security-Policy needs to allow framinghttps://n.exchange(frame-src https://n.exchangeor looser). If you see nothing, confirm the<script type="module">tag actually loaded (network tab) and that<nexchange-widget>is on the page after it. - TypeScript can't find types for
@n.exchange/widget/react- make sure yourtsconfig.json'smoduleResolutionisbundlerornode16/nodenext(not the legacynode), so subpathexportsresolve correctly. - Widget doesn't resize when its content changes - this is handled
automatically via a
resizepostMessage from the embed page; if you've wrapped the widget in a container with a fixedheight, remove it and size only thewidth(see the Quick start examples above).
How it works
WidgetController (src/controller.ts) is the only place the actual
protocol lives - both the <nexchange-widget> custom element (src/vanilla.ts)
and the <NexchangeWidget> React component (src/react/) are thin wrappers
over the same class:
- It creates an iframe pointed at
{baseUrl}/embed(n.exchange's own order-flow UI, calling n.exchange's v2 API same-origin - not the partner's page). - Once the iframe posts
{ type: 'ready' }, the controller replies once with{ type: 'init', apiKey, referralCode, address? }, targeted at the embed origin specifically (never a'*'wildcard), so the API key can't leak to an unexpected origin even if the iframe'scontentWindowsomehow pointed elsewhere. - It listens for
orderCreated/statusChanged/error/resizemessages from the iframe and translates them into the callbacks/events documented above, only accepting messages whoseevent.sourceis that same iframe'scontentWindow.
This mirrors how Stripe Elements / Plaid Link isolate a sensitive credential inside a same-origin iframe rather than handing the partner's own page JS realm anything beyond the one-time handshake.
Development
npm install
npm test
npm run buildpackage.json's overrides.rollup pins rollup to @rollup/wasm-node
instead of the default native-binary build. tsup's bundling depends on
rollup internally, and rollup's native binary crashes with a Bus error in
some sandboxed/containerized environments (a documented class of issue for
@rollup/rollup-* native packages). The WASM build is slightly slower for
large projects but functionally identical, and this package is small
enough that the difference is negligible - remove the override if you
confirm your CI/build environment doesn't need it.
License
MIT © n.exchange
