@funkit/connect-js

Framework-agnostic build of the Funkit Connect SDK
1,242
Install
npm install @funkit/connect-js
Latest Version:1.1.0
License:MIT
Last Updated:Oct 15, 2026
Links: npm
Maintainer: chloe-tan

Install

pnpm add @funkit/connect-js

Two things to import: the code, and the stylesheet. The stylesheet is the fallback the modal uses when shadow-DOM encapsulation is unavailable, so always ship it.

Svelte

Importing the package on the server is safe, so use a normal top-level import. Creating the instance still needs a real document — createFunkitCheckout() throws during server rendering — so call it from the browser only.

Create one shared SDK instance for the application:

// src/lib/funkit.ts
import {
  type FunkitCheckoutHandle,
  createFunkitCheckout,
} from '@funkit/connect-js'

let funkit: FunkitCheckoutHandle | undefined

// Built on first use, so it doesn't matter which component asks first
export function getFunkit(): FunkitCheckoutHandle {
  funkit ??= createFunkitCheckout({
    apiKey: '…',
    appName: 'Variational',
    walletConnectProjectId: '…',
    theme: 'dark',
  })
  return funkit
}

export function destroyFunkit(): void {
  funkit?.destroy()
  funkit = undefined
}

Initialize it from the root layout:

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount, onDestroy, type Snippet } from 'svelte'
  import '@funkit/connect-js/styles.css'
  import { destroyFunkit, getFunkit } from '$lib/funkit'

  let { children }: { children: Snippet } = $props()

  onMount(() => void getFunkit())
  onDestroy(() => destroyFunkit())
</script>

{@render children()}

Routes and components reuse that instance. Each component owns and removes its event subscriptions:

<script lang="ts">
  import { onMount } from 'svelte'
  import { getFunkit } from '$lib/funkit'

  // `on()` returns its own unsubscribe, which is what onMount wants for cleanup
  onMount(() =>
    getFunkit().on('checkout:success', (result) =>
      console.log('deposited', result),
    ),
  )
</script>

<button
  onclick={() =>
    getFunkit().openCheckout({
      checkoutItemTitle: 'USDC on Base',
      targetChain: '8453',
      targetAsset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
      targetAssetTicker: 'USDC',
      targetAssetAmount: 5,
    })}
>
  Deposit
</button>

Update the shared instance when the application theme changes; do not destroy and recreate it:

getFunkit().update({ theme: isDarkMode ? 'dark' : 'light' })

For a branded theme, use the same pure factories as @funkit/connect:

import { createFunkitCheckout, darkTheme } from '@funkit/connect-js'

const brandTheme = darkTheme({
  accentColor: '#ff5a19',
  customColors: {
    modalBackground: '#181818',
    primaryText: '#ffffff',
  },
})

const funkit = createFunkitCheckout({
  // ...
  theme: brandTheme,
})

Custom theme objects are opaque: only pass the exact object returned by lightTheme() or darkTheme(). Manually constructed, cloned, or serialized theme objects are unsupported and may fail when Connect renders them. Store and reuse the factory result when passing it to update({ theme }).

The same ownership model applies to Vue and Angular: initialize once at the application root and share the handle. destroy() is for full application teardown and tests, not route navigation.

Vite HMR: Some framework/plugin update boundaries can re-evaluate the singleton module without first destroying the mounted SDK instance. In that case, the module-scoped funkit variable resets and a second createFunkitCheckout() call correctly triggers the one-instance guard. This only affects those development-time HMR paths, not production builds or every Vite integration. If you encounter it, preserve the handle with Vite's import.meta.hot.data or destroy the existing instance before recreating it.

API

createFunkitCheckout(config)

Mounts the SDK on the page and returns a handle. Create one instance in the root application lifecycle and reuse it across routes and components. A second active instance throws; call destroy() before creating another.

Field Required Notes
apiKey yes Your Funkit API key
appName yes Shown in wallet prompts. 32 characters max
walletConnectProjectId From WalletConnect Cloud. Omit it and the wallet picker skips WalletConnect QR codes and mobile deep links; browser extensions, the adopted wallet and Coinbase are unaffected
appDescription, appUrl, appIcon Shown alongside appName in wallet prompts. Description falls back to appName, URL to the current page, and no icon is sent unless you give one
theme 'light', 'dark', or a complete theme returned by lightTheme(options) or darkTheme(options); omitted follows the system color scheme
locale e.g. 'en', 'ko', 'zh'
modalSize 'compact', 'medium', 'wide' or 'extraWide'
rpcUrls Your own HTTPS RPC endpoints keyed by chain id, e.g. { 8453: 'https://…' }. Pass an array for one chain to try endpoints in order. Chains you omit keep the SDK's defaults
initialChain Chain id the wallet is asked to be on when it connects, e.g. 8453. Defaults to Ethereum mainnet — set it to the chain your users check out on so they skip a network-switch prompt
adoptedWallet A wallet your page already holds, adopted at startup — see Adopting a host wallet
debug Verbose SDK logs in the console
textCustomizations Partial plain-data equivalent of Connect's text customization configuration
uiCustomizations Plain-data UI customization fields. React component slots are intentionally unavailable; use confirmationScreen.destinationConfig.iconSrc for destination branding
prefillFiatEmail, disableLogging, source Same meaning as funkitConfig in @funkit/connect

Adopting a host wallet

If your page already holds a wallet connection — your own connect button, a WalletConnect session inside your modal library, anything the SDK can't discover on its own — hand it over at creation time and the SDK adopts it: the wallet is listed in the connect modal and connected automatically at startup, so the user lands in checkout already connected without picking a wallet a second time.

const funkit = createFunkitCheckout({
  // ...the usual config
  adoptedWallet: {
    provider,             // the live EIP-1193 provider your app holds; must
                          // have request, on, and removeListener functions
    name: 'MetaMask',     // shown in the wallet list
    rdns: 'io.metamask',  // reverse-DNS wallet id
    iconUrl: 'https://…', // optional; generic wallet icon when omitted
  },
})

How it behaves:

  • Your wallet is the only wallet. Setting adoptedWallet turns off the SDK's own session restore, so a wallet remembered from a previous visit can never race or override the one your page hands over. The SDK always agrees with what your page shows the user — connected to the adopted wallet, or not connected at all.
  • Chain alignment. Connecting asks the wallet for initialChain. A wallet that can't or won't switch (e.g. a WalletConnect session scoped to fewer chains) still connects on its own chain; checkout switches on demand later.
  • Failure falls back. If the provider rejects, the SDK is simply disconnected — even for a returning user, since no previous session is restored — and the normal connect modal remains available. openCheckout() and openWithdrawal() called while adoption is still connecting wait for it to settle first. If adoption fails, withdrawal requires another connected wallet or a supplied wallet parameter.
  • De-duplication. The rdns also matches the wallet against EIP-6963 announcements, so the same wallet is never listed twice.
  • checkout:login-required never fires for a user whose wallet was adopted — they are already connected. Keep the subscription if you have one; it still covers users whose adoption failed.
  • disconnect() is unchanged: it clears only the SDK's session. Your page's own wallet connection is untouched.

adoptedWallet is creation-time only — to hand over a wallet acquired later, destroy() and create a new instance.

The handle

Method What it does
openCheckout(params) Opens the checkout modal. Resolves with { isActivated } once the SDK has taken the request
openWithdrawal(params) Opens the withdrawal modal. Resolves once the SDK has taken the request; rejects if no wallet is connected and none is supplied
openConnect() Opens the wallet connection modal. Rejects when a wallet is already connected, and also while a page-load session restore is still settling — the two carry different messages
disconnect() Disconnects the wallet and clears the SDK session. Resolves once getAccount() reads as disconnected; openConnect() becomes available on the next render. Call it from your app's own logout
getAccount() Returns the latest { isConnected, address? } wallet snapshot
update({ theme }) Changes the mounted SDK theme without resetting wallet or checkout state
on(event, handler) Subscribe to an event below. Returns the unsubscribe function
destroy() Unmounts the SDK and removes everything it added to the page. Safe to call twice

Calls made before the SDK has finished mounting wait for it; they don't fail. After destroy(), openCheckout, openWithdrawal, openConnect and disconnect reject, update throws, and on logs a warning and returns a no-op unsubscribe — a destroyed instance never emits events.

openCheckout(params)

The React SDK's FunkitCheckoutConfig, minus fields that are deprecated or customer-specific — test/types.test-d.ts names each exclusion. Callback-valued fields (title resolvers, action generators, minimum-deposit functions) are plain functions and work from any framework.

Field Required Notes
checkoutItemTitle yes e.g. 'USDC on Base'
targetChain yes Chain id as a string, e.g. '8453'
targetAsset yes Destination token address
targetAssetTicker yes e.g. 'USDC'
targetAssetAmount Human units. Omit to let the user choose
iconSrc, modalTitle, modalTitleMeta Modal header. modalTitle also takes a resolver function receiving the target token and active routing id
sourceTokenSymbol, sourceChain, sourceTokenAddress Preselect the source token. The type requires all three together
dynamicRoutingId, dynamicTargetAssetCandidates, bridgeOverride Dynamic routing, agreed with Fun.xyz for multi-route integrations
customRecipient Pay out to a different recipient. Not recommended unless the Fun.xyz team advises it
disabledSourceTokens Tokens that can't be used as the payment source
generateActionsParams, qrcodeActionType Checkout with custom action: contract calls to execute with the deposit. Actions take pre-encoded rawCalldata (the React SDK's ABI form needs viem's types)
addToWalletToken Token to offer adding to the wallet on the success screen
getMinDepositUSD Minimum USD needed to deposit, per asset/route
destinationYieldInfo Display-only lending information shown on the confirmation screen
resolveDestinationLineItems Synchronous callback returning custom confirmation rows; only the first three are displayed
Custom confirmation rows

Pass resolveDestinationLineItems to openCheckout alongside your deposit parameters. It receives the destination token amount as a string in human units, plus sourceTokenSymbol, sourceTokenAddress, and sourceChainId (each nullable). Return plain objects; no React dependency is needed.

await funkit.openCheckout({
  ...depositParams,
  resolveDestinationLineItems: ({ targetAssetAmount }) => [
    {
      id: 'deposit-amount',
      label: {
        text: 'Deposit amount',
        tooltip: 'Amount arriving in the destination token.',
      },
      value: { text: targetAssetAmount, color: 'default' },
    },
  ],
})

For margin usage rows, load your account snapshot before opening checkout and calculate the projected usage synchronously from that snapshot and targetAssetAmount. Your app owns the calculation, formatting, and translations. Use distinct stable id values for each row.

Both label and value accept a tooltip. Value colors are default, success, caution, or danger. Set a row's isLoading to show a skeleton; quote loading also skeletons values. Return null or [] to hide the rows. A throwing resolver is caught by the SDK, which logs a warning and hides the rows.

The resolver runs when the destination amount or source-token details change. Changes to your app's account data alone do not refresh the rows, including isLoading; update() currently only updates the theme. Async resolvers and independent host-data refresh are not supported. Before an exact-input quote arrives, the destination amount is unknown and the resolver is not called.

openWithdrawal(params)

Moves a token out of the user's wallet to an address they choose, on any supported chain. The token has to be in the wallet by the time the withdrawal executes, but it doesn't have to start there: preWithdrawalAction runs first and can move it in, and withdrawalSourceTokenBalance reports what is withdrawable in the meantime. The React SDK's WalletWithdrawalConfig, minus the multi-source dropdown — test/types.test-d.ts names each exclusion.

await funkit.openWithdrawal({
  modalTitle: 'Withdraw',
  sourceTokenSymbol: 'USDC',
  sourceChainId: '42161',
  sourceTokenAddress: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
})

By default, the connected wallet signs and sends. That includes an adopted wallet: openWithdrawal() called while adoption is still connecting waits for it to settle first. With nothing connected and no wallet supplied, the call rejects. Connect a wallet with openConnect(), configure adoptedWallet when creating the SDK, or supply the wallet parameter for this withdrawal.

The returned promise resolves when the SDK accepts the request, before the withdrawal completes. Subscribe to withdrawal:confirmation and withdrawal:error for execution results.

Field Required Notes
modalTitle yes Modal header. modalTitleMeta and iconSrc are optional extras
sourceTokenSymbol, sourceChainId, sourceTokenAddress yes The token leaving the wallet. Chain id as a string, e.g. '42161'
withdrawalSourceTokenBalance Returns the balance shown as withdrawable, replacing what the SDK reads from the wallet. Use it when the funds are still in your system and only arrive in the wallet during preWithdrawalAction
preWithdrawalAction Runs after the quote is accepted and before anything is sent. Return transactions to prepend to a supplied wallet’s sendTransactions batch, or [] after a side effect such as moving funds into the wallet. Throw to abort with your message
onBeforeSign Runs when the user commits, before any signature. Throw to abort
getMinWithdrawalUSD, getMinWithdrawalAmount, getMaxWithdrawalUSD Amount limits
defaultReceiveToken, disableConnectedWallet Receive-side defaults
swappedTargetTokenAddress, swappedTargetChainId, swappedTargetTokenSymbol Fiat off-ramp token, when it differs from the source token
resolveWithdrawalFeeItems Synchronous callback returning custom rows for the transaction breakdown; only the first three are displayed
wallet A FunkitWithdrawalWallet that executes this withdrawal. Omit to use the connected wallet, including an adopted one — see below
Custom breakdown rows

Pass resolveWithdrawalFeeItems to openWithdrawal to add rows, such as a projected margin requirement, to the withdrawal screen's transaction breakdown. They appear above the SDK's own fee rows and look identical to them. It receives:

  • sourceAmount, sourceTokenSymbol, sourceChainId: what the user is withdrawing. sourceAmount is the amount being quoted, in source-token units, after any fee the SDK deducts before quoting. It matches what the user typed unless the withdrawal method charges a flat fee.
  • receiveAmount, receiveTokenSymbol, destinationChainId: the quoted arrival. receiveAmount is null while a quote loads or when there is none.
await funkit.openWithdrawal({
  ...withdrawalParams,
  resolveWithdrawalFeeItems: ({ sourceAmount }) => [
    {
      id: 'margin-after',
      label: 'Margin after withdrawal',
      value: projectMargin(sourceAmount),
      tooltip: 'Collateral left on your account after this withdrawal.',
    },
  ],
})

Rows are display-only: they never change the breakdown's total. Text is shown as provided, without translation, and each row needs a distinct stable id. Return null or [] to hide the rows. A throwing resolver is caught by the SDK, which logs a warning and hides the rows. The resolver runs once the user has entered an amount, and again when the amount, the quote or the selected tokens change; changes to your app's data alone do not refresh the rows. Rows sit inside the collapsible breakdown, so they are visible once the user expands it, or immediately with uiCustomizations.withdrawalScreen.autoExpandFeesSummary.

Supplying your own wallet

Supply openWithdrawal({ ...params, wallet }) only when the connected wallet can't execute the withdrawal itself. The wallet must be an EVM account that can send transactions and sign typed data and messages; relayers or non-EVM signers that can't do all three are not supported here. This is separate from adoptedWallet: adoption connects an EIP-1193 provider at SDK creation, while the wallet parameter supplies execution methods for one withdrawal and does not change the SDK’s connected account.

The parameter accepts a plain object or a class instance implementing FunkitWithdrawalWallet. Methods retain their original this binding. The shape follows the React SDK's WithdrawalClient with two differences:

  • signTypedData and signMessage are required. Gasless routes (permit, EIP-3009) ask for a signature instead of a transaction, and the withdrawal aborts without one. They are routine for USDC.
  • confirmTransaction resolves with { status: 'success' | 'reverted' }. A full receipt is optional; the adapter supplies the transaction hash it waited on. An ethers receipt reports status: 1 | 0 and does not compile here — otherwise a reverted withdrawal would read as a success. A viem receipt already has this shape; from ethers, map it: status === 1 ? 'success' : 'reverted'.

sendTransactions is optional, and it is the only way to batch. With it, an approval and the withdrawal go out together and the user signs once. Without it, each transaction is sent on its own and the user signs each one. Either way, it must return a transaction hash that confirmTransaction can wait on.

The connected wallet — what you get when you omit wallet — sends one transaction at a time. Batching is only available on a wallet you supply.

That is also why preWithdrawalAction can only return transactions when you supply a wallet with sendTransactions. With the connected wallet, return [].

Events

Event Payload
checkout:open —
checkout:close { isNewDeposit, isSoftHidden? }
checkout:success { type: 'success', message, metadata }
checkout:error { type: 'error', message, metadata }
checkout:validation { isValid, message }
checkout:estimation The complete checkout quote and fee estimation
checkout:confirmation Checkout ID string
checkout:login-required { onLoginFinished? } for resuming a custom host login. Subscribing takes over the login flow: the SDK then shows nothing and waits for the host. With no subscriber, a disconnected openCheckout() opens the wallet connection modal automatically
checkout:blocked { blockedReason }
withdrawal:confirmation { withdrawalId, formData } once the withdrawal transaction is sent. withdrawalId is the destination address for this wallet-based flow. formData carries the destination address, chain and token, the amount, and the quote
withdrawal:error { type: 'error', message, metadata }
account:change { isConnected, address? }, emitted only when either value changes

The withdrawal modal also emits checkout:close when it closes, but not checkout:open; there is no withdrawal:success event.

getAccount() reads as disconnected until the bridge mounts. The initial disconnected snapshot is not emitted as account:change.

How it's built

  • dist/index.js — ESM for bundlers, code-split so wallet SDKs load on demand
  • dist/styles.css — the SDK stylesheet, re-exported as @funkit/connect-js/styles.css
  • dist/index.d.ts — self-contained declarations; no @funkit/connect install needed for types. The public types are a hand-maintained contract, not a copy of the SDK's: test/types.test-d.ts pins shared fields to the SDK's definitions and lists every upstream field that is deliberately unsupported, so an SDK change forces an explicit decision here. test/pack.test.ts packs the tarball into an empty project and runs tsc with skipLibCheck: false.

@funkit/connect, React and the wallet libraries are devDependencies on purpose: they're compiled in, so listing them as dependencies would make every consumer install a second copy they never use.

Development

pnpm --filter=@funkit/connect-js build   # esbuild + bundled .d.ts + typecheck
pnpm --filter=@funkit/connect-js test    # smoke tests, type-contract tests, packed-package tsc check

Related Packages

marqy

High-fidelity loops for modern interfaces.

837,868
crd-ui

Credit/debit card preview for payment forms. Zero-dependency vanilla core with R...

15,227 35
dialkit

Live controls for tuning interfaces in React, Solid, Svelte, Vue, and plain Java...

4,094,012 1,273
rizzo-css

A modern CSS design system with semantic theming, 14 themes, accessible componen...

11,599 1
@svel/cli-service

local service for svelte-cli projects

309 8

Version History

Version Released License
1.1.0 MIT
1.0.0 MIT
0.6.0 MIT
0.5.0 MIT
0.4.0 MIT