npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@affiliateo/elements-react

v1.2.0

Published

React components for embedding Affiliateo Elements (referral link, balance, payouts and identity) inside your own app.

Readme

@affiliateo/elements-react

React components for embedding Affiliateo Elements inside your own app. Your users get their referral link, watch their earnings, and cash out without ever leaving your product.

This is a thin, typed binding over the hosted embed.js script. The elements, security, and versioning live on the Affiliateo side, so a new element or theme token never means a package upgrade.

Install

npm install @affiliateo/elements-react

react and react-dom (18+) are peer dependencies.

How it works

  1. Your backend mints a session with your secret afk_ key.
  2. Your frontend hands the returned client secret to <AffiliateoElements>.
  3. Each element renders as an iframe on the Affiliateo domain, inside your page.

Your secret key never reaches the browser, and the affiliate's data never passes through your servers.

1. Mint a session (your backend)

One endpoint of your own that mints a session for the logged-in user. The afk_ key stays on your server.

// POST /api/affiliateo-session
app.post('/api/affiliateo-session', async (req, res) => {
  const r = await fetch(
    `https://affiliateo.com/api/v1/businesses/${SLUG}/apps/${APP_ID}/affiliates/embed-session`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.AFFILIATEO_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: req.user.email, // who this session is for
        components: ['affiliate', 'balance', 'withdraw'],
        allowed_origins: ['https://app.example.com'], // pages allowed to frame it
        // Optional: theme from first paint, and load a brand font.
        appearance: { colorPrimary: '#1754D8', borderRadius: '12px' },
        fonts: [{ family: 'Inter', src: 'https://cdn.example.com/inter.woff2' }],
      }),
    },
  )
  res.json({ client_secret: (await r.json()).client_secret })
})

2. Mount (your frontend)

Wrap the part of your app that shows elements in one <AffiliateoElements> provider, then drop in the elements you want.

'use client'

import {
  AffiliateoElements,
  AffiliateElement,
  BalanceElement,
  WithdrawElement,
} from '@affiliateo/elements-react'

export function AffiliatePanel() {
  return (
    <AffiliateoElements
      fetchClientSecret={() =>
        fetch('/api/affiliateo-session')
          .then((r) => r.json())
          .then((d) => d.client_secret)
      }
    >
      <AffiliateElement />
      <BalanceElement />
      <WithdrawElement />
    </AffiliateoElements>
  )
}

fetchClientSecret is a function, not a string, so the SDK can call it again on its own: it re-mints shortly before each hourly session expiry (and on tab wake after a sleep) and swaps the element to the new session. A page left open keeps working — just make sure the function mints a fresh secret each call.

You never set the height. Each element measures itself and resizes its own frame; initialHeight is only the placeholder shown until the first measurement arrives.

Which to use, per screen

Elements and the REST API are designed to mix, and which side a screen belongs on is mostly a performance question. Every element is its own iframe, so a page stacking four pays for four loads before it paints and your app cannot cache any of them. Anything the API already returns is faster built yourself and can be cached, so the screen renders on open.

Build link, stats, products, activity and balance from GET /affiliates?email= + GET /apps/{appId} + GET /affiliates/conversions. Mount elements only for withdraw, payouts and identity: the three that collect bank details, open the account-wide wallet, or run ID capture. Mounting everything still works, it is just slower and uncacheable.

The recurring figures are on the elements too. <StatsElement> and <AffiliateElement> show MRR, ARR, subscriptions and not-renewing beneath clicks/sales/earned whenever that affiliate has subscriptions, and render nothing at all when they do not. Same GET /affiliates?email= read through the same code, so an element and a hand-built screen cannot report different MRR for one person. The one figure no element shows is churn_rate: whoever is looking is the affiliate, and a percentage on their own traffic is a score, so it stays owner-side.

The QR needs no building either. links.qr on that same response is a plain public image URL, so the whole thing is one <img>:

<img src={affiliate.links.qr} alt="Referral QR code" width={256} height={256} />

https://affiliateo.com/qr/{short_code}.svg (or .png, which is what email clients need — they strip inline SVG). No key, no session, no SDK, cached forever. It also sends Access-Control-Allow-Origin: *, which is what a Download button actually requires: a cross-origin <a download> is ignored by browsers and navigates to the file instead, so you have to fetch it into a blob first.

async function download(url) {
  const blob = await fetch(url).then((r) => r.blob())
  const href = URL.createObjectURL(blob)
  Object.assign(document.createElement('a'), { href, download: 'referral-qr.png' }).click()
  URL.revokeObjectURL(href)
}

Optional ?dark= and ?light= take hex colours, and ?size= sizes the raster. The pair must stay dark-on-light with real contrast or the request is a 400: a low-contrast or inverted code is one that will not scan, and finding that out after the flyers are printed is not a recoverable mistake. The image encodes the SHORT link tagged ?s=qr, so scans separate from link taps in the affiliate's click breakdown without you doing anything.

The qr ELEMENT is still there and still the right call when you want our styled card with its copy-and-download affordances rather than a bare image.

Lists are the sharpest case. The embedded activity and payouts elements show the 25 most recent and neither paginate nor filter, so an affiliate with a year of sales cannot reach the 26th and cannot ask to see just their refunds, while GET /affiliates/conversions is cursor-paginated (?limit=, ?starting_after=) and filterable by date (?from=&to=) and by type (?type=refund,chargeback, comma-separated, any of subscription, one_time, renewal, trial, refund, chargeback).

Filter on our side, not in yours. Both filters are applied in the query, upstream of the cursor, so a filtered feed pages through matching rows only. Filtering rows you have already fetched looks equivalent and is not: it can only search what is on screen, so a Refunds chip finds nothing for someone whose single refund sits 300 rows down, and the chip reads as broken. An unknown ?type= value comes back 400 rather than an empty page, so a typo can never be mistaken for "this affiliate has no refunds".

In our own apps that becomes one menu beside the list heading, five entries over six raw types: All types, Sales (?type=subscription,one_time), Renewals (renewal), Trials (trial), Refunds (?type=refund,chargeback). Whether a first sale renews, and whether a reversal was the customer asking or the bank taking, are not distinctions an affiliate filters on: both mean money back. The rows still say Sale and Chargeback individually, so the difference stays visible where it is useful.

Copy is not the catch. The API returns data, never labels, but you do not have to write your own translations: the exact strings our elements use are published at https://affiliateo.com/locales/{lang}.json in all 16 languages. Lift embed.appEarnings.* for the buckets and their hint line, embed.activity.* for row types and empty states, embed.filter.preset.* for the date chips, and embed.link.format* for the picker. The embed.activity.* type words double as the labels for a Type menu, since a row reading "Refund" and a filter offering "Refund" should not be two different translations of the same word. Copy the values into your own catalogue at build time rather than fetching live, since those files are the elements' runtime asset and not a versioned API. Keep the money words verbatim even if you reword the rest: Paid means already in their Affiliateo balance and not yet in their bank, so "Paid out" is the wording to avoid.

Components

| Component | Shows | Confirms identity | | --- | --- | --- | | <AffiliateElement> | Referral link + clicks, sales, earned, and MRR/subscriptions | No | | <LinkElement> | Just the referral link, with a format dropdown | No | | <QrElement> | A scannable QR code of the referral link | No | | <StatsElement> | Clicks, sales and total earned, plus MRR/ARR/subscriptions when they have any | No | | <ProductsElement> | Your catalogue with per-product commission | No | | <ActivityElement> | Recent sales; refunds as negatives | No | | <BalanceElement> | Wallet: ready, pending, settling | Yes | | <WithdrawElement> | Balance + a button to cash out | Yes | | <IdentityElement> | Get verified and paid-ready | Yes |

The three money/identity components open a small confirmation window on affiliateo.com before they render, because an Affiliateo wallet is per person and spans every program the affiliate is in. Withdrawing and identity checks continue in that same window, so your page never rebuilds bank collection or ID capture and never needs camera permission.

Each accepts initialHeight, title, className, and style. For a component chosen at runtime, use <AffiliateoElement component="balance" />.

<WithdrawElement> additionally accepts flow="inline", which renders the full cash-out form in place (amount, payout speed, bank, fees) instead of the balance plus a button that opens the hosted portal in a popup. The popup stays the default because the sign-in confirm works best top-level; opt in when a popup would jar on your page.

When the sign-in lapses

One confirm covers every gated element and keeps covering it: confirming signs the person in on Affiliateo's side, so the other gated elements on the page recognise that session with no second email, and so does the next visit. The step-up itself lasts an hour and lives only in the element's memory; the sign-in behind it outlives that.

When it eventually lapses the element goes back to asking, and says so. Only worth handling if YOUR page remembers the signed-in state somewhere:

<BalanceElement onLocked={() => setSignedIn(false)} />

Without it, whatever you unhid on the strength of that memory keeps showing next to a login form.

Make it feel instant

Mount every element when your page opens and show or hide them as tabs switch, instead of mounting each one on tab-click. A hidden element still loads, so its tab opens already rendered with fresh data. This is safe for the money elements too: pre-mounted, they simply show their confirm gate until the person uses them.

Showing your own skeleton over an element? Hide it on onReady, not on the iframe's load event: load fires when the document arrives, which is before the element has fetched its data and laid out, so a placeholder removed there uncovers an empty box. onReady fires after layout, with your appearance already applied, and again after any reload (a finished withdrawal, an hourly session refresh).

<BalanceElement onReady={() => setLoading(false)} />

Theming

Pass appearance at mint for a first paint already in your brand (no flash). To restyle live, for example on a dark-mode toggle, pass appearance to the provider and update it:

If your page is dark, send colorBackground. An element paints a ground only when you give it one, and that ground covers the whole element. Leave it out and the element stays transparent, which is supported, but the browser's own backdrop shows wherever your content does not reach, and that backdrop is white.

That is worth stating because it is invisible while you build. A white backdrop behind a light page looks exactly like a correct element, so a dark theme can ship looking perfect in development and render as a white slab for every real visitor. If you theme dark at all, send the token.

colorScheme (light or dark) is the companion and is usually unnecessary: it is inferred from your colorText, since light text means a dark page. Send it only when that inference is wrong.

Anything unusable is dropped rather than failing the mint, and comes back in ignored_appearance_keys. Check that field first when a colour does not appear.

<AffiliateoElements fetchClientSecret={...} appearance={{ colorBackground: '#191919', colorText: '#eee' }}>

Colours, gradients, fonts, sizes, radii, spacing, and button and link styling are all tokens. colorBackground, colorSurface, and buttonColor also take a CSS gradient. The full list is in the docs. Custom fonts are loaded at mint (the fonts array above), not through appearance.

Server components

The provider and elements are client components (they use browser APIs and the DOM). In Next.js App Router, render them from a file with 'use client', or import them into a client component. Mounting only happens in the browser.

Native apps (iOS / Android)

For a native app, do not use this package. Mint the session with "platform": "native" (omit allowed_origins) and open the returned element_urls entry top-level in a WKWebView or android.webkit.WebView. See the docs.

Troubleshooting

  • Blank element. Your page's origin is probably not in allowed_origins. The browser console will say the frame was refused.
  • "Session expired". Make sure fetchClientSecret mints a fresh secret each call rather than returning a cached string.
  • Confirmation window blocked. It opens from a user tap; a pop-up blocker will stop one opened on load.

License

MIT