@funkit/connect-rn
v1.0.4
Published
React Native bindings for @funkit/connect. Re-exports the headless @funkit/connect-core logic; the RN platform halves (Restyle theme, WebView Swapped transport, Transfer/Swapped screens, logging) land in later releases.
Readme
@funkit/connect-rn
React Native bindings for Funkit Connect. This README is for working on the
package. For integrating the SDK into an app (install, peer dependencies,
FunkitProvider API, DepositFlowConfig), see the client integration guide —
maintained in Notion. To run the SDK on a simulator/device, use the
with-expo playground.
Architecture
@funkit/connect-rn is the React Native half of a three-package split. The
headless business logic lives once in
@funkit/connect-core and is shared by web and RN, so the two
platforms can't drift:
| Package | Role |
| --- | --- |
| @funkit/connect-core | Headless, platform-agnostic — domains, utils, query/Statsig hooks, theme factories, FunLogger. No DOM, vanilla-extract, or wagmi under src/**. |
| @funkit/connect | Web presentation halves (.tsx, vanilla-extract, Dialog/portals, iframe transport, wagmi identity). |
| @funkit/connect-rn | This package — the RN presentation halves over core. |
The platform halves this package supplies (mirroring connect's web halves):
- a local Shopify Restyle theme +
useFunkitTheme()/useColorScheme() FunLoggerwired to a React Native log transportWebViewSwappedTransport+ the Swapped screen- the managed Add money → Transfer / UDA → QR flow
Core stays platform-agnostic by depending on contracts that each platform
implements: IdentityProvider (web wraps wagmi; RN takes host props),
SwappedTransport (web <iframe>; RN WebView), and LogTransport (web
Datadog browser-logs; RN console). The customer theme is a plain ThemeVars
object from core that RN maps to a Restyle theme via toRestyleTheme(tokens).
Public surface. index.ts re-exports core plus an explicit, named set of
RN symbols (no barrels): the provider, the useFunkitCheckout entry hook, theme
tokens/adapter/primitives, and config types. The screens, Statsig RN bindings,
log transports, identity context, and transfer hooks are internal (mirroring
@funkit/connect, which exports none of them). When adding a public export, wire
it explicitly in index.ts.
Peer dependencies
These are declared as peer dependencies (not bundled), so the host app links a
single copy and controls the native-module versions. Editing peerDependencies
in package.json is a public-API change — bump it deliberately and add a
changeset (a peer bump is a major change for consumers).
The authoritative list lives in package.json (peerDependencies /
peerDependenciesMeta) — it's intentionally not duplicated here so it can't
drift. Adding a peer dependency is a consumer-facing decision: every integrator
must install it. Before adding one:
- Pick it carefully. Prefer a widely-used, well-maintained library — ideally one (nearly) every customer already depends on — over a niche package that adds install burden for everyone. A peer dep is a tax on all consumers.
- Update the client docs. A new peer dep changes the install/setup steps integrators follow, so the client integration guide (Notion) must be updated in the same change.
- Native modules need extra care. A native peer (iOS/Android native code, vs JS-only) can't be linked by the host without a rebuild, carries version compatibility constraints against the RN core, and may be absent in some runtimes. Treat it as a heavier commitment than a JS-only dep.
- Prefer Expo-Go-bundled native modules. When a native peer is needed, prefer one already bundled in Expo Go (see Expo Go compatibility) so the SDK keeps working in the playground without a custom dev build. This is a preference, not a hard requirement — if a non-bundled native module is genuinely needed, make it optional + lazy + guarded with a host override (the way clipboard is).
- Don't depend on the Expo SDK. "Bundled in Expo Go" is a convenience for our
playground, not an assumption about consumers — many integrate via bare React
Native, not Expo. Prefer a framework-agnostic RN library over one that needs the
Expo SDK (the
expopackage) installed to work, so it runs either way. Anexpo-*name isn't itself a red flag — those modules are published by Expo but run fine in bare RN; the concern is a library that genuinely requires the Expo runtime, like@expo/ui. For example,@gorhom/bottom-sheetis preferred over@expo/ui's bottom sheet.
Expo Go compatibility
connect-rn is designed to run in Expo Go (no custom dev build needed) — this
is what lets the with-expo playground preview the flow
in Expo Go. Keep new code Expo-Go-safe.
Every native peer the SDK relies on is either bundled in Expo Go for the target
SDK (react-native-reanimated, react-native-gesture-handler,
react-native-webview, react-native-svg, react-native-safe-area-context,
AsyncStorage) or JS-only (@gorhom/bottom-sheet, react-native-qrcode-svg).
Two deliberate choices keep it that way:
- Clipboard is optional, lazy, and guarded.
@react-native-clipboard/clipboardis a bare-RN native module not in Expo Go.src/transfer/clipboard.tsonlyimport()s it when a copy actually happens (never at startup) inside a try/catch — so in Expo Go, wheregetEnforcing('RNCClipboard')would throw, a copy degrades to a no-op + warning rather than crashing. A host injects its own writer viaFunkitProvider'scopyToClipboard(e.g.expo-clipboard), and that override runs instead, so the throwing path never executes. - No
react-native-device-info.src/statsig/statsigMetadata.tsvendors the Statsig metadata adapter with thereact-native-device-infodependency removed (it's a native module that needs a dev build). The app-version / device-model fields are leftundefined;systemNameandlocaleare read from RN's built-inPlatform/NativeModules(no extra native dep). Geo/IP resolution uses plainfetch.
Contributor rule: don't add a hard top-level import of a bare-RN native module that Expo Go doesn't bundle. If you genuinely need one, make it optional + lazily imported + guarded with a host-injectable override, the way clipboard is.
Local development
No root shortcut exists for this package — use the
pnpm --filterform (orcd packages/connect-rnand drop the filter).
pnpm --filter @funkit/connect-rn build # esbuild bundle + typecheck
pnpm --filter @funkit/connect-rn build:watch # rebuild on change
pnpm --filter @funkit/connect-rn dev # build:watch + type-gen watch in parallel
pnpm --filter @funkit/connect-rn typecheck # tsc --noEmit
pnpm --filter @funkit/connect-rn lint # oxlint (lint:fix to auto-fix)
pnpm --filter @funkit/connect-rn storybook # web Storybook on http://localhost:7007Typical inner loop when changing the SDK and watching it in the app: run
pnpm --filter @funkit/connect-rn dev in one terminal and
pnpm --filter with-expo start in another. Metro watches the whole monorepo, so
SDK edits hot-reload in the playground.
Testing
Tests run with Vitest in the node environment (no DOM — native modules
are stubbed). Components mount with react-test-renderer + act(), not React
Testing Library.
pnpm --filter @funkit/connect-rn test # run once
pnpm --filter @funkit/connect-rn test -- --watch # watch mode
pnpm --filter @funkit/connect-rn test -- test/providers/FunkitProvider.test.tsx # one file- Config:
vitest.config.ts. Setup:test/setup.ts(stubs globalfetch, enablesIS_REACT_ACT_ENVIRONMENT). - Native-module stubs live in
test/*.stub.ts(x)(react-native,react-native-svg,react-native-reanimated,@gorhom/bottom-sheet, clipboard, webview, qrcode). Pulling in a new native module? Add a stub or the import fails under node. - Mock at the
@funkit/api-baseedge — never mock internal hooks (useTransferInit, etc.). Snapshot tests may mock internal hooks to freeze inputs. See the repo TESTING.md.
Fonts
connect-rn's theme renders text in the system UI font by default (SF on iOS,
Roboto on Android) at two weights: 400 (regular) and 500 (medium). There is
no semibold/bold in the token set. The default needs no font assets — it
works out of the box.
connect-rn is font-agnostic: it ships no typeface and no font preset. To
render in a custom font (the Fun Mobile design uses Inter), supply your own
fonts tokens and register the matching font files (see "Rendering in a custom
font" below).
connect-rn does not — and cannot — load fonts itself. A JS-only React
Native library has no way to register a font with the native font manager: fonts
are either bundled and linked natively (iOS UIAppFonts / Android assets/fonts)
or loaded at runtime through a native module such as
expo-font. Both live in the
host app, not in this package.
Rendering in a custom font
To render in a custom font instead of the system default, set fonts on your
tokens and register the matching font files.
You can't select a custom font's weight via fontWeight in React Native/Expo:
Android matches custom fonts by exact family-name string, and a font loaded at
runtime registers one native typeface per name — so fontWeight: '500' on a
custom Inter family is silently ignored. connect-rn therefore expresses weight
through the family name (set family + a per-weight familySuffix), and the
host registers each cut under its own name — e.g. Inter-Regular and
Inter-Medium. Use fontFamilyForWeight(...) to resolve those names so you don't
hardcode them.
Define the font tokens in your host app, load the files before rendering
<FunkitProvider> (if a name is missing, that text falls back to the system
font), and build your theme with them. Example with Expo (expo-font +
@expo-google-fonts/inter):
import { useFonts, Inter_400Regular, Inter_500Medium } from '@expo-google-fonts/inter'
import {
type FunkitFontTokens,
fontFamilyForWeight,
lightTokens,
toRestyleTheme,
} from '@funkit/connect-rn'
// Host-owned: weight lives in the family name (RN/Expo can't pick a cut via
// fontWeight). cssWeight is kept for the Swapped WebView (CSS) theme.
const interFonts: FunkitFontTokens = {
family: 'Inter',
weights: {
base: { familySuffix: 'Regular', cssWeight: '400' },
medium: { familySuffix: 'Medium', cssWeight: '500' },
},
}
const themes = {
light: toRestyleTheme({ ...lightTokens, fonts: interFonts }),
// …dark likewise
}
export default function App() {
const [fontsLoaded] = useFonts({
[fontFamilyForWeight(interFonts, 'base')]: Inter_400Regular, // 'Inter-Regular'
[fontFamilyForWeight(interFonts, 'medium')]: Inter_500Medium, // 'Inter-Medium'
})
if (!fontsLoaded) return null // or a splash screen
return <FunkitProvider themes={themes} {...}>{/* … */}</FunkitProvider>
}