@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.
Maintainers
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-webviewOn Expo, install the WebView through Expo so you get the build matched to your SDK version:
npx expo install react-native-webviewreact, 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=512It 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 elementLists 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 WithdrawElementCopy 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:
appearancewhen you mint the session styles the very first paint, which is what stops a flash of our blue before your brand lands.appearanceas 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
NSCameraUsageDescriptiontoInfo.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". Awebsession 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
fetchClientSecretevery 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
onLockedonly 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].
