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

@funkit/connect-rn

v9.2.0

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()
  • FunLogger wired to a React Native log transport
  • WebViewSwappedTransport + 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.

Withdrawal display currency

Set WithdrawalFlowConfig.displayCurrency to 'EUR' to enter crypto withdrawal amounts in euros. Users can switch between EUR and source-token units. The conversion uses the token's USD price and the shared USD-to-EUR rate, without assuming that EURC trades at exactly one euro. For example, at $2 per token and 0.8 EUR per USD, entering €8 stages 5 tokens for withdrawal.

Token amounts round down to the token's precision; the typed fiat value is preserved through denomination switches and navigation. Use Max still stages the exact allowed token amount. Rate refreshes do not rewrite an existing entry. Until usable rates are available, entry stays in clearly labelled token units; a rate arriving later does not reinterpret an amount already typed. Cached rates remain usable if a background refresh fails.

Omitting the option, or setting 'USD', preserves existing USD entry behavior. Limits, quotes, confirmation screens, and settlement retain their existing denominations. Configure withdrawal ceilings separately using the maximum callbacks; a EUR ceiling does not implicitly select an entry currency.

Payment deposit metadata

clientMetadata.authorizedAmountBaseUnit is an explicit maximum gross deposit in destination-token base units, forwarded unchanged to POST /eoa. For Base EURC, '20000000' caps the gross deposit at 20 EURC before fees. It is separate from initialAmount (fiat input), lockAmount (UI editing), and actual net credit; no cap is inferred. Supply a positive integer string of 1–30 digits without leading zeros, or beginDeposit throws FunkitMisconfiguredError before opening. The field is optional in the SDK; Betty's backend requires it with the payment IDs and rejects a different cap or destination when reusing the same payment ID.

When lockAmount is set and this cap is supplied, the transfer screen sizes the amount to send from a routing quote for the selected source token: the price-feed estimate is quoted, scaled so the route delivers the cap, rounded up, then grown by the lockeddepositbufferbps Statsig config (default 30 bps, targetable per customer via the Statsig user's apiKey). The buffer is skipped when the quoted route returns the sent amount in the same asset (same token on the same chain, or the same symbol across chains). If the quote or its route inputs are unavailable, the screen falls back to the price-feed estimate plus the same buffer and logs a warning. Locked deposits without this cap keep the plain price-feed estimate.

MoonPay crypto setup

MoonPay requires a secure globalThis.crypto.getRandomValues implementation on Hermes. Install it in your app entry before importing @funkit/connect-rn or loading your app. The SDK supports either setup below; it does not install a native crypto module for you. See the MoonPay SDK README.

Expo

Install the version compatible with your Expo SDK:

pnpm exec expo install expo-crypto

Create polyfills.ts:

import { getRandomValues } from 'expo-crypto'

if (typeof globalThis.crypto?.getRandomValues !== 'function') {
  const crypto = globalThis.crypto ?? ({} as Crypto)
  Object.defineProperty(crypto, 'getRandomValues', {
    value: getRandomValues,
    configurable: true,
    writable: true,
  })
  if (globalThis.crypto !== crypto) {
    Object.defineProperty(globalThis, 'crypto', {
      value: crypto,
      configurable: true,
      writable: true,
    })
  }
}

Load it first in your entry. For Expo Router, use import './polyfills' followed by import 'expo-router/entry'. For a custom Expo entry:

import './polyfills'
import { registerRootComponent } from 'expo'
import App from './App'

registerRootComponent(App)

Bare React Native

Install react-native-get-random-values with pnpm add react-native-get-random-values, install iOS pods, and rebuild the native app. Make this the first import in your entry, before importing your app:

import 'react-native-get-random-values'

MoonPay 1.17.0 includes the about:srcdoc WebView origin allowance. Consumers no longer need a dependency patch for MoonPay's authorization frame.

Identity verification permissions (iOS)

Sumsub's document capture photographs an ID, takes a selfie and uploads documents such as proof of address, so iOS hosts must declare both usage strings:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "Used to photograph your ID and take a selfie when identity verification is required for a deposit.",
        "NSPhotoLibraryUsageDescription": "Used to upload a photo of your ID or a document such as proof of address when identity verification is required for a deposit."
      }
    }
  }
}

In bare React Native, add the same two keys to ios/<App>/Info.plist.

Sumsub's native SDK (optional)

Sumsub's identity capture runs in a WebView by default. Its WebSDK takes no colors, fonts or radii from code, so it never matches your theme. Install Sumsub's mobile SDK and the same step opens Sumsub's native screens, themed from your connect-rn theme (light/dark colors, brand overrides, Inter, button and card radii).

@sumsub/react-native-mobilesdk-module is an optional peer. Without it, or in a binary that doesn't link it (Expo Go included), Sumsub stays in the WebView, unchanged. Sumsub's screens present modally over your app, so our back button and step dots don't show on them; closing Sumsub before submitting counts as a cancel.

Expo

npx expo install @sumsub/react-native-mobilesdk-module

Add connect-rn's config plugin, and embed the Inter cuts natively with expo-font — Sumsub's native screens can't see a font loaded at runtime:

{
  "expo": {
    "plugins": [
      [
        "expo-font",
        {
          "fonts": [
            "node_modules/@expo-google-fonts/inter/400Regular/Inter_400Regular.ttf",
            "node_modules/@expo-google-fonts/inter/500Medium/Inter_500Medium.ttf",
            "node_modules/@expo-google-fonts/inter/600SemiBold/Inter_600SemiBold.ttf"
          ]
        }
      ],
      [
        "@funkit/connect-rn",
        {
          "sumsub": {
            "cameraPermission": "Used to photograph your ID and take a selfie.",
            "microphonePermission": "Used to record a short video when asked.",
            "photoLibraryPermission": "Used to upload a photo of your ID or a document.",
            "locationPermission": "Used to confirm your country when asked."
          }
        }
      ]
    ]
  }
}

The plugin:

  • adds Sumsub's CocoaPods spec repo (https://github.com/sumsub/Specs.git) and the CocoaPods CDN to the Podfile;
  • adds Sumsub's Maven repository (https://maven.sumsub.com/repository/maven-public/) to the Android project;
  • sets NSCameraUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription and NSLocationWhenInUseUsageDescription. Each sumsub option overrides its key; without one, a reason your app already declares is kept, and a default is written otherwise.

Every option is optional, so "@funkit/connect-rn" alone works. Pass { "sumsub": false } to skip the native setup. Then rebuild the native app (npx expo prebuild / expo run:ios).

Bare React Native

Follow Sumsub's React Native setup: the two source lines at the top of the Podfile, the Maven repository, and the four Info.plist keys above. Bundle the Inter .ttf files natively (UIAppFonts on iOS, android/app/src/main/assets/fonts on Android), named Inter-Regular, Inter-Medium and Inter-SemiBold as the theme uses them.

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 expo package) installed to work, so it runs either way. An expo-* 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-sheet is 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/clipboard is a bare-RN native module not in Expo Go. src/transfer/clipboard.ts only import()s it when a copy actually happens (never at startup) inside a try/catch — so in Expo Go, where getEnforcing('RNCClipboard') would throw, a copy degrades to a no-op + warning rather than crashing. A host injects its own writer via FunkitProvider's copyToClipboard (e.g. expo-clipboard), and that override runs instead, so the throwing path never executes.
  • Stripe is an optional peer, loaded on demand. @stripe/stripe-react-native is a native module not in Expo Go, and it throws at import time on a binary without its pod. Nothing names the package outside the guarded import() in src/fiat/providers/stripe/stripeSdk.ts, which StripeOnrampBridge runs only after a host passed stripeOnramp to FunkitProvider. A host that never configures Stripe never installs it and never links it. A host that wants Stripe installs it, enables its Onramp module (Expo: the config plugin with includeOnramp: true; bare RN: stripe-react-native/Onramp in the Podfile and StripeSdk_includeOnramp=true in gradle.properties), and rebuilds; that rail cannot run in Expo Go.
  • Sumsub's native SDK is an optional peer, loaded on demand. The capture step checks for its native module first and import()s @sumsub/react-native-mobilesdk-module only when it is linked (src/fiat/providers/sumsub/sumsubSdk.ts); otherwise Sumsub runs in the WebView.
  • The glass module is optional at runtime. FunkitGlassEffect (this package's ios/) is not in Expo Go. src/native/glassEffect.ts looks it up with requireOptionalNativeModule, so an unlinked binary renders the expo-blur fallback (which Expo Go bundles) instead of throwing.
  • No react-native-device-info. src/statsig/statsigMetadata.ts vendors the Statsig metadata adapter with the react-native-device-info dependency removed (it's a native module that needs a dev build). The app-version / device-model fields are left undefined; systemName and locale are read from RN's built-in Platform / NativeModules (no extra native dep). Geo/IP resolution uses plain fetch.

Contributor rule: don't add a hard top-level import of a 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 — or, for a native view of our own, look it up as an optional module the way the glass view is.

Sheet material (blur / Liquid Glass)

The V2 deposit sheet and the drawers over it (fee details, country list, KYC choosers) are drawn on glass: the design's Figma layer is a dark fill with a Glass effect, which is iOS 26's Liquid Glass. The SDK ships that material itself (src/flow/SheetMaterial.tsx): a blur of the host app with Liquid Glass refracting through it, drawn by one native view, FrostedGlassView, so the blur and the glass share a single corner. The tint is the material's — every surface renders it as is, and none passes one in. The deposit-received toast draws its card on the same view.

The view lives in a native module this package carries under ios/ (FunkitGlassEffect). It also exports GlassView, forked from expo-glass-effect with iOS 26's container-concentric corners added, which the SDK uses alone for controls floating over content. Expo autolinking picks the module up from expo-module.config.json; a host rebuilds its binary once after upgrading. Below iOS 26 FrostedGlassView draws the blur without glass. Where the pod isn't linked — Android, Expo Go, a binary built before the upgrade — src/native/glassEffect.ts reads the module as absent and the surfaces draw an expo-blur blur instead. Wherever there is no glass, each surface paints its fill over the blur.

expo and expo-blur are peer dependencies for this. A host that wants a different native material still has FunkitProvider's renderBackdropBlur, paired with liquidGlass when that view is Liquid Glass; the SDK then paints no fill over it.

Lighter withdrawals

Lighter lives on its own entry, @funkit/connect-rn/clients/lighter, so a host that never configures it carries none of it. You call the hook and pass the methods it returns to beginWithdrawal — the SDK's withdrawal config names no venue. The methods replace the generic crypto row on the method screen:

| | Fast | | --- | --- | | Source | USDC Perps | | Destination | any supported chain/token | | Path | Fun + Relay | | Speed | seconds | | Limits | $4 min, $3M max |

import { useFunkitCheckout } from '@funkit/connect-rn'
import {
  type LighterWithdrawalFlowConfig,
  useLighterWithdrawal,
} from '@funkit/connect-rn/clients/lighter'

const lighter: LighterWithdrawalFlowConfig = {
  // The end user's L1 address. The SDK resolves their L2 main-account index
  // from it — Lighter addresses accounts by index, not by address.
  l1Address,
  // Your Lighter L2 signer. The SDK never holds L2 key material.
  signerClient,
  // Reserved for the Native Bridge rail, which has not shipped — see below.
  sendLighterSecureWithdrawal: ({ amountTokenUnits, assetIndex, routeType }) =>
    client.withdraw({ amount: amountTokenUnits, assetIndex, routeType }),
  // Your own accounting: margin requirements and locked collateral are not
  // visible to the SDK.
  withdrawalSourceTokenBalance: ({ sourceToken }) =>
    balances[sourceToken.symbol] ?? 0,
  // Awaited before the rail signs. Lighter strategies and withdrawals share
  // one L2 signing account, so cancel in-flight strategies here or the
  // withdrawal signs against a nonce that has already moved. Throwing aborts
  // the withdrawal and consumes no nonce.
  onBeforeSign: () => cancelOpenStrategies(),
}

// Empty until the L2 account resolves — both rails address it by index, so the
// flow falls back to its generic crypto row until then.
const methods = useLighterWithdrawal({
  lighter,
  config: { ...withdrawalConfig, withdrawalWallet },
})

const { beginWithdrawal } = useFunkitCheckout()
beginWithdrawal({ ...withdrawalConfig, withdrawalWallet, methods })

The unstaked fee

Lighter charges a flat $3 on Fast withdrawals for users not sufficiently staked in its LIT pool. The SDK reads the stake off the account and takes the fee off the amount before quoting — so the quote prices what Lighter will actually move, and the user is not promised ~3 tokens the transfer never delivers.

That timing is the whole contract: quoteInputFeeTokens is deducted pre-quote, and a fee taken from a quote's result is a different number at a different time. If a venue ever charges that way it needs its own field rather than reusing this one.

It gets no row in the fee summary. The quote already prices the smaller amount, so its own cost rows carry what the withdrawal costs; a venue that bills its fee as source-chain gas instead sets extraSourceGasUsd, which lands in the network cost row.

While the account is still loading the fee holds at 0 — "no shares" and "not a staker" are indistinguishable then, and charging would bill an exempt user. A failed lookup fails closed and charges, because Lighter takes the fee whether or not the SDK could see the stake.

The Native Bridge rail is not available yet. Only Fast is reachable — its row is the sole Lighter method on the screen. sendLighterSecureWithdrawal is required by the config type and reserved for that rail, so it must be supplied even though nothing calls it today; a stub is fine. Same for the optional getSecureMinWithdrawalAmount.

Solana withdrawals

Solana lives on its own entry, @funkit/connect-rn/clients/solana, so a host that never configures it carries none of it. The host owns the Solana wallet connection; the SDK takes the connected wallet's signer and builds a withdrawal config over it. USDC on Solana is the only source today, and both the crypto and the cash (Swapped) methods work from it.

import { useFunkitCheckout } from '@funkit/connect-rn'
import { useSolanaWithdrawal } from '@funkit/connect-rn/clients/solana'
import { useMemo } from 'react'

// Both inputs are compared by identity, so keep them stable across renders.
const solana = useMemo(() => ({ signer: solanaWallet }), [solanaWallet])
const config = useMemo(
  () => ({
    defaultReceiveToken: 'USDC',
    // Your own accounting for the spendable balance, if you have it.
    withdrawalSourceTokenBalance: () => usdcBalance,
  }),
  [usdcBalance],
)
// `solanaWallet`: a base58 address, the RPC connection funkit submits through,
// and one of `signTransaction` / `signAndSendTransaction`.
const withdrawal = useSolanaWithdrawal({ solana, config })

const { beginWithdrawal } = useFunkitCheckout()
// `undefined` while the signer is absent or its address is not base58.
if (withdrawal) beginWithdrawal(withdrawal)

Install @solana/web3.js (an optional peer) and give Hermes what it lacks before importing the SDK: Buffer (the buffer package), a full URL (react-native-url-polyfill/auto), and crypto.getRandomValues as described under MoonPay crypto setup. Point the connection at an RPC you control; Solana's public endpoints rate-limit.

Local development

No root shortcut exists for this package — use the pnpm --filter form (or cd packages/connect-rn and 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:7007

Typical 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 through test/testRenderer.ts (a DOM-free test-renderer adapter) + act(), not React Testing Library. It tracks host nodes only, so query by tag string ('view', 'text', 'pressable') rather than by component reference.

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 global fetch, enables IS_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-base edge — 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 — Inter-Regular, Inter-Medium and Inter-SemiBold. 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,
  Inter_600SemiBold,
} 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' },
    semibold: { familySuffix: 'SemiBold', cssWeight: '600' },
  },
}

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'
    [fontFamilyForWeight(interFonts, 'semibold')]: Inter_600SemiBold, // 'Inter-SemiBold'
  })
  if (!fontsLoaded) return null // or a splash screen
  return <FunkitProvider themes={themes} {...}>{/* … */}</FunkitProvider>
}