@stake-sdk/ui
v1.1.8
Published
Stake UI widgets for React Native with Material Design 3 theming
Readme
@stake-sdk/ui
Stake UI SDK for React Native. It ships three widgets — AccountWidget, CreditBuilderWidget, and NeighborhoodCashWidget — plus shared runtime helpers. Theming uses Material Design 3 via react-native-paper.
Widgets decode a host-minted JWT (token) and never take a residentId prop. The payload must include credit_builder_id; that value is used on /resident/{id}/... API calls.
Installation
npm install @stake-sdk/ui react-native-paper @tanstack/react-query axios react-native-confetti-cannon react-native-maps react-native-safe-area-context react-native-svg
# or
yarn add @stake-sdk/ui react-native-paper @tanstack/react-query axios react-native-confetti-cannon react-native-maps react-native-safe-area-context react-native-svgPeer dependencies (must be present in your app):
| Package | Version |
| ------- | ------- |
| react | ≥18 |
| react-native | ≥0.70 |
| react-native-paper | ≥5 |
| @tanstack/react-query | ≥5 |
| axios | ≥1.19 |
| react-native-confetti-cannon | ≥1.5 |
| react-native-maps | ≥1.20 |
| react-native-safe-area-context | ≥4 |
| react-native-svg | ≥13 |
| fidel-react-native | ≥3.2.1 (optional — card linking only) |
Expo apps typically already include @expo/vector-icons. react-native-maps requires a native rebuild after install. fidel-react-native is optional and must be used in a dev/production build (not Expo Go).
Setup
Call Stake.open once (for example in a root layout useEffect) before any widget that hits Stake APIs. The SDK does not store credentials: you pass an async getAuthToken provider and your app owns fetching, caching, and refreshing the token. The SDK calls getAuthToken() per request and getAuthToken({ forceRefresh: true }) once after a 401, then retries the request.
import { Stake } from "@stake-sdk/ui";
import type { StakeAuthTokenProvider } from "@stake-sdk/ui";
const getAuthToken: StakeAuthTokenProvider = async ({ forceRefresh } = {}) => {
return await fetchTokenFromYourBackend({ forceRefresh });
};
await Stake.open({
environment: "development", // "development" | "sandbox" | "production"
getAuthToken,
googlePlacesApiKey: process.env.EXPO_PUBLIC_GOOGLE_PLACES_API_KEY,
// Optional: take over Plaid Hosted Link (Custom Tabs / Safari / expo-web-browser).
// Return true so the SDK does not also call Linking.openURL.
// onOpenHostedLink: (url) => { void WebBrowser.openBrowserAsync(url); return true; },
onSuccess: (message) => console.log(message),
onError: (message) => console.error(message),
});Mint the OAuth token on your own backend and hand it to getAuthToken. Do not ship the client secret in a public EXPO_PUBLIC_* variable. The JWT must include credit_builder_id (the resident identifier). Pass that same JWT as token on each widget.
Stake.open primes the token once so mint failures surface through onError. Every Stake API request also sends API-Version: 2026-08-01; mint tokens against the same version.
StakeConfig
| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| environment | "production" \| "sandbox" \| "development" | Yes | Selects the Stake API base URL. |
| getAuthToken | StakeAuthTokenProvider | Yes | Returns a bearer token. Called with { forceRefresh: true } after a 401. |
| googlePlacesApiKey | string | No | Google key used for address autocomplete and as the fallback Maps key for Neighborhood Cash. |
| coreApiKey | string | No | Optional Core dealsAPIKey header for legacy Core routes. Neighborhood Cash does not need this. |
| onOpenHostedLink | (url: string) => boolean \| void \| Promise<boolean \| void> | No | Open the Plaid Hosted Link URL yourself. Return true (or a Promise) to take over; otherwise the SDK uses React Native Linking.openURL. Same contract as the Web SDK. |
| onSuccess | (message: string) => void | No | Called after a successful Stake.open. |
| onError | (message: string) => void | No | Called if Stake.open fails (the promise also rejects). |
Linking a bank (Plaid Hosted Link)
Cash out, Venmo, and Connected Accounts need a linked checking account. The SDK asks Stake for a Hosted Link session and opens the returned URL. Nothing from Plaid is bundled into the SDK.
By default the URL is opened with React Native Linking.openURL (system Safari / Chrome). Completion is detected by polling linked accounts for about two minutes. When the resident returns to the app, the SDK also polls immediately.
For an in-app browser (Chrome Custom Tabs, SFSafariViewController, expo-web-browser), supply onOpenHostedLink(url) on Stake.open and return true. Do not await the browser dismiss inside the handler if you want polling to run while it is open — fire-and-forget and return true. Do not open Hosted Link in a WebView; banks block those.
Google Maps / Places
Address autocomplete (Request paper check and Stake Checking onboarding) calls the Places Autocomplete and Place Details HTTP APIs. Enable Places API in Google Cloud, set EXPO_PUBLIC_GOOGLE_PLACES_API_KEY in the host app, and pass the same value to Stake.open as googlePlacesApiKey. You can override it per widget with AccountWidget's googlePlacesApiKey.
Neighborhood Cash map previews and Near You pan/zoom need the Maps SDK for Android / iOS (and typically Maps Static API). Configure the key in native Expo config (android.config.googleMaps / ios.config.googleMapsApiKey) and rebuild. Pass googleMapsApiKey on NeighborhoodCashWidget to override the Stake.open key.
Neighborhood Cash native setup
Hosts that ship NeighborhoodCashWidget must:
- Install peer
react-native-maps, wire Google Maps API keys in Expo config, and rebuild the native app. - Install optional peer
fidel-react-native≥ 3.2.1 in a dev/production build (not Expo Go) for card linking.
The widget talks to resident-scoped Stake APIs (/resident/{credit_builder_id}/neighborhood_cash/...) with the same OAuth bearer token. The token must include Neighborhood Cash scopes such as nc_get_link_data, nc_get_cards, nc_delete_card, nc_get_offers, nc_get_rewards, and consumer_deals_get.
Usage
Every widget requires an MD3Theme and a JWT token. When token is missing or cannot be decoded into a credit_builder_id, the widget renders nothing and calls onError with an error message (and null once a valid token resolves).
Account widget
Balance, activity, unclaimed cash-back claiming, paper check, cash out, Venmo transfer, connected accounts (Plaid Hosted Link), and Stake Checking onboarding.
import { AccountWidget } from "@stake-sdk/ui";
import { useTheme } from "react-native-paper";
function MyScreen() {
const theme = useTheme();
return (
<AccountWidget
theme={theme}
token={token}
onError={(message) => {
if (message) console.error(message);
}}
googlePlacesApiKey={process.env.EXPO_PUBLIC_GOOGLE_PLACES_API_KEY}
onContactSupport={() => {
/* host opens chat, mailto, or a support URL */
}}
/>
);
}Or with withTheme:
import { AccountWidget } from "@stake-sdk/ui";
import { withTheme } from "react-native-paper";
import type { MD3Theme } from "react-native-paper";
function MyScreen({ theme }: { theme: MD3Theme }) {
return <AccountWidget theme={theme} token={token} />;
}
export default withTheme(MyScreen);googlePlacesApiKey is optional. When omitted, the value passed to Stake.open is used. onContactSupport is called from read-only Stake Checking steps; the SDK never opens a URL, chat widget, or dialer itself.
Credit builder widget
Eligibility, opt-in / opt-out, and the credit builder tracker.
import { CreditBuilderWidget } from "@stake-sdk/ui";
import { useTheme } from "react-native-paper";
function MyScreen() {
const theme = useTheme();
return (
<CreditBuilderWidget
theme={theme}
token={token}
onError={(message) => {
if (message) console.error(message);
}}
/>
);
}Neighborhood Cash widget
Neighborhood Network hub, offers, Near You map, cash-back history, and Fidel card linking.
import { NeighborhoodCashWidget } from "@stake-sdk/ui";
import { useTheme } from "react-native-paper";
function MyScreen() {
const theme = useTheme();
return (
<NeighborhoodCashWidget
theme={theme}
token={token}
onError={(message) => {
if (message) console.error(message);
}}
onUnavailable={() => {
/* hide your Neighborhood Cash entry point */
}}
onDismiss={() => {
/* user backed out of Hub — navigate away */
}}
onContactSupport={() => {
/* cash-back detail “Contact Support” */
}}
googleMapsApiKey={process.env.EXPO_PUBLIC_GOOGLE_PLACES_API_KEY}
/>
);
}Set useMockData to skip Core/Fidel APIs and render local preview data (token is then optional). initialRoute can start on a named screen ("apply", "hub", "allOffers", "nearYou", "cashBack", "linkedCards"); live mode hides the widget when the offer is not enabled and remaps "apply" to the hub.
Theming helpers
Pass any MD3Theme from react-native-paper. Stake defaults and a font-family helper are exported:
import {
StakeLightTheme,
StakeDarkTheme,
withFontFamily,
} from "@stake-sdk/ui";
const theme = withFontFamily(StakeLightTheme, "YourBrandSans");Load the typeface in the host (for example expo-font / useFonts) before rendering widgets. The SDK never fetches fonts — unlike the Web SDK there is no theme.fontUrl.
A single family name is enough when one file covers every weight. For a face with separate files per weight (Raleway, most Google Fonts), pass a map so Android and StyleSheet fontWeight resolve the right cut:
import {
useFonts,
Raleway_400Regular,
Raleway_500Medium,
Raleway_600SemiBold,
Raleway_700Bold,
} from "@expo-google-fonts/raleway";
const [loaded] = useFonts({
Raleway_400Regular,
Raleway_500Medium,
Raleway_600SemiBold,
Raleway_700Bold,
});
const theme = withFontFamily(StakeLightTheme, {
regular: "Raleway_400Regular",
medium: "Raleway_500Medium",
semibold: "Raleway_600SemiBold",
bold: "Raleway_700Bold",
});Button labels
withButtonLabels maps to the Web SDK's buttonTextTransform and
buttonLetterSpacing tokens. They default to as-authored copy and
normal tracking. The two are independent — uppercase does not add
tracking on its own. Set both for all-caps labels; emToLetterSpacing
converts the Web SDK's em value at the 14px button size:
import {
StakeLightTheme,
withButtonLabels,
emToLetterSpacing,
} from "@stake-sdk/ui";
const theme = withButtonLabels(StakeLightTheme, {
buttonTextTransform: "uppercase",
buttonLetterSpacing: emToLetterSpacing(0.04),
});buttonTextTransform accepts "none", "uppercase", "capitalize",
or "lowercase". buttonLetterSpacing is a number (React Native
pixels), not a CSS em string. Tokens apply to action buttons only —
not titles, body copy, or links.
Brand vs CTA fill
Paper colors.primary tints links, selected tabs, snackbars, and
loaders. Contained action buttons use that same role unless you stamp
accentFill / accentText — the Web SDK's split when button fill is
not the brand accent (Progress: teal chrome, lime buttons).
import {
StakeLightTheme,
withBrandTokens,
} from "@stake-sdk/ui";
const theme = withBrandTokens(StakeLightTheme, {
accentFill: "#8BCB02",
accentText: "#01444B",
heading: "#01444B",
});Omit the tokens and contained buttons keep colors.primary. ctaFill
/ ctaLabel / headingColor read these keys with MD3 fallbacks.
Exports
| Export | Description |
| ------ | ----------- |
| Stake | Stake.open(config) — initialize SDK / auth for the session |
| StakeConfig | Type for Stake.open configuration |
| StakeAuthTokenProvider | Type for the async getAuthToken provider |
| StakeOpenHostedLinkHandler | Type for Stake.open({ onOpenHostedLink }) |
| AccountWidget | Account UI: balance, activity, cash-out flows, connected accounts, Stake Checking |
| CreditBuilderWidget | Credit builder UI: eligibility, opt-in/out, and tracker |
| NeighborhoodCashWidget | Neighborhood Cash UI: apply/hub, offers, map, cash-back, card linking |
| NeighborhoodCashWidgetProps | Props type for NeighborhoodCashWidget |
| NeighborhoodScreenName | Named screens accepted by initialRoute |
| CashBackEntry, LinkedCard, NeighborhoodOffer, NeighborhoodRoute, OfferChannel | Neighborhood Cash data types |
| StakeLightTheme / StakeDarkTheme | Paper-compatible theme objects |
| withFontFamily | Apply a host-loaded typeface (string or per-weight map) to every MD3 type role |
| FontWeightFamilies | Type for the per-weight map passed to withFontFamily |
| withButtonLabels | Apply buttonTextTransform / buttonLetterSpacing to action-button labels |
| withBrandTokens | Optional accentFill / accentText / heading when CTA fill ≠ primary |
| ctaFill / ctaLabel / headingColor | Read brand tokens with MD3 fallbacks |
| BrandTokenOptions | Type for withBrandTokens |
| emToLetterSpacing | Convert Web SDK em tracking to RN letterSpacing at 14px |
| buttonLabelStyle | Style object to spread onto a custom action-button label |
| ButtonTextTransform / ButtonLabelOptions | Types for withButtonLabels |
Props
AccountWidget
| Prop | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| theme | MD3Theme | Yes | Material Design 3 theme from react-native-paper |
| token | string | Yes | Host-minted JWT with credit_builder_id |
| googlePlacesApiKey | string | No | Overrides the Stake.open key for address autocomplete |
| onError | (message: string \| null) => void | No | Token validity: error message when invalid, null when resolved. Invalid tokens render nothing. |
| onContactSupport | () => void | No | Stake Checking “Contact Support”. The host decides how support is surfaced. |
CreditBuilderWidget
| Prop | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| theme | MD3Theme | Yes | Material Design 3 theme from react-native-paper |
| token | string | Yes | Host-minted JWT with credit_builder_id |
| onError | (message: string \| null) => void | No | Token validity: error message when invalid, null when resolved. Invalid tokens render nothing. |
NeighborhoodCashWidget
| Prop | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| theme | MD3Theme | Yes | Material Design 3 theme from react-native-paper |
| token | string | No | JWT with credit_builder_id. Required unless useMockData is true. |
| onError | (message: string \| null) => void | No | Token validity (skipped in mock mode). Invalid tokens render nothing. |
| onUnavailable | () => void | No | Resident is not in the Neighborhood Network program (neighborhood_network missing or is_enabled is not true). Widget renders nothing. |
| onDismiss | () => void | No | User backed out of the root screen (Hub). |
| onContactSupport | () => void | No | Cash-back detail “Contact Support”. |
| googleMapsApiKey | string | No | Maps key for hub / Near You. Falls back to Stake.open({ googlePlacesApiKey }). |
| useMockData | boolean | No | Skip Core/Fidel APIs and render local mock data (UI preview only). Default false. |
| initialRoute | NeighborhoodScreenName | No | Override starting screen. Live mode still hides the widget when the offer is not enabled. |
Publishing (for maintainers)
Package name: @stake-sdk/ui. Source lives under packages/stake-sdk/.
From the monorepo root:
| Script | Description |
| ------ | ----------- |
| npm run build:packages | Build the UI package (output in packages/stake-sdk/dist/). |
| npm run version:sdk:patch | Bump patch version. Creates a git commit and tag in the workspace package. |
| npm run version:sdk:minor | Bump minor version. |
| npm run version:sdk:major | Bump major version. |
| npm run publish:sdk | Build and publish the current version to npm. |
| npm run release:sdk:patch | Build, bump patch, then publish. |
| npm run release:sdk:minor | Build, bump minor, then publish. |
| npm run release:sdk:major | Build, bump major, then publish. |
From packages/stake-sdk:
npm run build
npm run publish:package # npm publish --access publicOne-step release from root (example — patch):
npm run release:sdk:patchEnsure you are logged in (npm login) with publish access to the @stake-sdk scope, and use --access public for the first scoped publish. To change the npm name or scope, edit name in packages/stake-sdk/package.json.
