@funkit/connect-js
v0.5.0
Published
Framework-agnostic build of the Funkit Connect SDK
Readme
Install
pnpm add @funkit/connect-jsTwo 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
funkitvariable resets and a secondcreateFunkitCheckout()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'simport.meta.hot.dataor 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
adoptedWalletturns 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()andopenWithdrawal()called while adoption is still connecting wait for it to settle first. If adoption fails, withdrawal requires another connected wallet or a suppliedwalletparameter. - De-duplication. The
rdnsalso matches the wallet against EIP-6963 announcements, so the same wallet is never listed twice. checkout:login-requirednever 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: ({ targetAssetHumanAmount }) => [
{
id: 'deposit-amount',
label: {
text: 'Deposit amount',
tooltip: 'Amount arriving in the destination token.',
},
value: { text: targetAssetHumanAmount, color: 'default' },
},
],
})For margin usage rows, load your account snapshot before opening checkout and calculate the projected usage synchronously from that snapshot and targetAssetHumanAmount. 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 |
| wallet | | A FunkitWithdrawalWallet that executes this withdrawal. Omit to use the connected wallet, including an adopted one — see below |
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:
signTypedDataandsignMessageare 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.confirmTransactionresolves with{ status: 'success' | 'reverted' }. A full receipt is optional; the adapter supplies the transaction hash it waited on. An ethers receipt reportsstatus: 1 | 0and 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 demanddist/styles.css— the SDK stylesheet, re-exported as@funkit/connect-js/styles.cssdist/index.d.ts— self-contained declarations; no@funkit/connectinstall needed for types. The public types are a hand-maintained contract, not a copy of the SDK's:test/types.test-d.tspins 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.tspacks the tarball into an empty project and runstscwithskipLibCheck: 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