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-native

v1.2.0

Published

React Native components for embedding Affiliateo Elements (referral link, balance, payouts and identity) in your mobile app.

Readme

@affiliateo/elements-react-native

Embed Affiliateo Elements in a React Native app. Each element renders in a react-native-webview; there is no native-UI rewrite, because withdraw and identity are money and camera surfaces safest run once, on Affiliateo's side, and reused everywhere.

Not to be confused with @affiliateo/react-native, which is the affiliate tracking SDK. This package renders the embedded UI elements.

Install

npm install @affiliateo/elements-react-native react-native-webview

On Expo, install the WebView through Expo so you get the build matched to your SDK version:

npx expo install react-native-webview

react, react-native, and react-native-webview (13.3+) are peer dependencies. There is no separate Expo package: this is the one, and it works in Expo Go, in development builds and on EAS, because react-native-webview is one of the native modules Expo Go already bundles.

Use it

Mint the session on your backend with your afk_ secret key and "platform": "native" (omit allowed_origins, since nothing frames a WebView), return the client_secret, then:

import { ScrollView } from 'react-native'
import { QrElement, BalanceElement } from '@affiliateo/elements-react-native'

const theme = { colorPrimary: '#16A34A', contentPadding: '16px' }

export function Earnings() {
  return (
    <ScrollView>
      <QrElement fetchClientSecret={mintOnMyBackend} appearance={theme} />
      <BalanceElement
        fetchClientSecret={mintOnMyBackend}
        appearance={theme}
        onComplete={(status) => {
          // "complete" when a withdrawal or verification finished
        }}
      />
    </ScrollView>
  )
}

Every element measures itself and this package sizes the view to match, so stacking them takes no hardcoded heights. Pass autoHeight={false} for an element that owns a whole screen and should take its height from your style.

Components: AffiliateElement, LinkElement, QrElement, StatsElement, ProductsElement, ActivityElement, BalanceElement, WithdrawElement, IdentityElement, or the generic <AffiliateoElement component="balance" ... />.

The recommended setup

Nine components is a lot of freedom, and freedom is not a layout. This is the one we ship in our own apps. Start here.

Which to use, per screen. Every element is its own page load, so a tab stacking four pays for four before it paints and your app cannot cache any of them. Anything the REST API already returns is faster built natively and can be cached, so the screen paints the moment it opens. Build link, stats, products, activity and balance yourself from GET /affiliates?email= + GET /apps/{appId} + GET /affiliates/conversions, and mount elements only for withdraw, payouts and identity: the three that collect bank details, open the account-wide wallet, or run ID capture.

The recurring figures are on the elements too. stats and affiliate show MRR, ARR, subscriptions and not-renewing beneath clicks/sales/earned whenever that affiliate has subscriptions, and draw nothing at all when they do not. They come from the 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. links.qr on that same GET /affiliates?email= response is a plain public image URL — https://affiliateo.com/qr/{short_code}.svg, or .png for a raster — so showing an affiliate's QR is one image view with no session, no token and no SDK:

https://affiliateo.com/qr/aB3xY9k.png?size=512

It needs no key, is cached forever, and sends Access-Control-Allow-Origin: * so you can fetch it into memory for a Share or Save sheet. Optional ?dark= and ?light= take hex colours; the pair must stay dark-on-light with real contrast or it is a 400, because a low-contrast or inverted code will not scan. The image encodes the SHORT link tagged ?s=qr, so scans show up separately 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.

Tab 1   Link       built from the API
Tab 2   Balance    built from the API
Tab 3   Cash out   withdraw            <- the only element

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.

Mounting all of them still works if you would rather we rendered everything. It is just slower and uncacheable:

Tab 1   Link       QrElement, LinkElement, StatsElement, ProductsElement
Tab 2   Balance    BalanceElement, ActivityElement
Tab 3   Withdraw   WithdrawElement

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.

Three tabs in a native bottom tab bar (expo-router's Tabs, or @react-navigation/bottom-tabs) rather than a hand-built one: you get the OS's own hit-testing, animation and accessibility, and on current iOS it is the floating pill people already know. Each tab is one ScrollView with its elements stacked, nothing between them, and your app's background behind them.

Do not give IdentityElement a tab of its own next to WithdrawElement. Withdraw already walks an unverified affiliate through the ID check exactly where they need it, and skips it forever once they pass. A separate tab shows them the same step twice. Use IdentityElement only as a standalone get-paid-ready page when you are not showing withdraw at all.

Props

| Prop | Type | What it does | | --- | --- | --- | | fetchClientSecret | () => Promise<string> \| string | Returns a secret minted by your backend. Called again before each expiry, so mint fresh rather than caching. | | appearance | Record<string, string> | Theme tokens applied to the live element. Pass the same map when you mint, so first paint is already yours. | | filter | boolean | false hides the built-in date filter on affiliate and activity. | | range | { from, to } \| null | Drive the date window from your own controls, as YYYY-MM-DD. null is all-time. Leave it undefined to keep our filter in charge. Pair with filter={false}. | | flow | 'inline' \| 'portal' | Withdraw only. Native renders the cash-out form in place by default; 'portal' opens the hosted flow instead. | | autoHeight | boolean | Defaults to true. Set false to size the element yourself. | | initialHeight | number | Placeholder height before the first measurement. Defaults to 180. | | onComplete | (status: string \| null) => void | A withdrawal or verification finished. | | onReady | () => void | The element rendered its first frame. Hide your own spinner here, not on the WebView's onLoadEnd: that fires before the element has laid out, so a placeholder removed there uncovers an empty box. Fires again after any reload. | | onLocked | () => void | A gated element is asking to sign in again. Only needed if you remember the signed-in state yourself. | | onError | (error: unknown) => void | Your fetchClientSecret failed, after retries. | | origin | string | Your Affiliateo origin. Defaults to https://affiliateo.com. | | style | StyleProp<ViewStyle> | Applied to the WebView. |

Theme it, or it will not match

Nothing about your app is visible from inside an element, so an element you never theme renders in Affiliateo's own colours. Two places, both worth setting:

  • appearance when you mint the session styles the very first paint, which is what stops a flash of our blue before your brand lands.
  • appearance as a prop restyles a live element, so a dark-mode switch does not have to reload it and lose whatever the person was in the middle of.

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.

contentPadding is worth calling out. Elements carry no outer margin, because on the web they already sit inside your padded layout. A WebView is edge to edge, so without contentPadding (or padding on the view around it) your element runs into both sides of the phone.

The full token list is at https://affiliateo.com/docs/elements.

Permissions

The identity element captures a live ID photo, so the app needs camera permission before that element mounts.

Expo, where you do not edit the native files at all. Put it in app.json / app.config.js and rebuild:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "Used to verify your identity so you can get paid."
      }
    },
    "android": { "permissions": ["CAMERA"] }
  }
}

Bare React Native:

  • iOS: add NSCameraUsageDescription to Info.plist.
  • Android: add <uses-permission android:name="android.permission.CAMERA" /> and request it at runtime.

Either way the OS blocks the camera without it and verification cannot finish.

Notes

  • Mint with "platform": "native". A web session expects framing origins and will not behave correctly top-level in a WebView.
  • The WebView is transparent, so elements sit on your app's background instead of a white card. If you replace this package with your own WebView, set a transparent background there too.
  • Sessions last an hour, and this package re-mints on its own about two minutes before each expiry, plus on the way back to the foreground if a backgrounded app slept through the moment. Return a freshly minted secret from fetchClientSecret every time it is called rather than a cached string.
  • The gated elements confirm identity in a window opened via window.open. This package hosts that window in a modal WebView layered over the element; the element stays mounted (and listening) underneath and receives the confirmation over a same-origin channel. When the window closes after a finished withdrawal/verification the element reloads to reflect it; when a plain confirm window closes, the element updates in place. Links inside that window (terms, privacy) open in the system browser.
  • One confirm unlocks the rest, and keeps them unlocked. Typing the emailed code into any gated element signs the person in, so the others mounted alongside it recognise that session silently instead of asking again, and so does the next app launch. The step-up itself lasts an hour and lives only in the element's memory; the sign-in behind it outlives that, which is what makes later opens silent.
  • When that sign-in eventually lapses, the element goes back to asking and says so. This package forgets its own state on it automatically. Add onLocked only if YOU remember the signed-in state somewhere: clear it there, or the rest of your screen keeps acting signed in beside a login form. That exact bug shipped in our own apps, where a stale flag left an unlocked sales feed sitting under a login.
  • Make it feel instant: render the elements for all your tabs when the affiliate screen mounts and hide the inactive ones with style={{ display: 'none' }}, rather than mounting each on tab press. Hidden that way a WebView has no viewport, which is what keeps a gated element from asking for a sign-in code before anyone has looked at it. A tab kept at full size off screen (a pager, or content below the fold) does count as seen. Even then only one code is ever sent: Affiliateo keeps a single live code per person, so simultaneous elements share it rather than racing to replace each other's.

Status: initial release, not yet verified on a physical device. Report problems to [email protected].