# Stellar AppKit > One SDK for every Stellar wallet — unified wallet API, Soroban built in, real transaction previews, and framework wrappers for React, Vue, Solid, and Svelte. > > npm: `@saganta/stellar-appkit` > GitHub: https://github.com/sagantaHQ/stellar-appkit > Docs: https://stellar-appkit.saganta.com > License: MIT Stellar AppKit is a Web3Modal / Reown AppKit equivalent for Stellar. It provides one unified wallet API, a first-class Soroban layer, real transaction previews instead of raw XDR, and a themeable UI that works identically dropped into any site. ## Install ```bash npm install @saganta/stellar-appkit @saganta/stellar-appkit-ui-web ``` That's it. All wallet SDKs and the Stellar SDK are bundled as regular dependencies — installed automatically, version-locked to known-working ranges, and tree-shaken out of your bundle if you don't use the corresponding connector. The UI package has two runtime dependencies: `qr-code-styling@^1.9.2` (styled QR codes for the WalletConnect pairing view — rounded data modules, extra-rounded finder pattern outer rings, circular finder pattern inner dots) and `motion` (used by the optional animation presets). The bottom-sheet drag-to-dismiss still uses a custom 30-line spring engine on native Pointer Events + `requestAnimationFrame`; the open/close transitions use native WAAPI. Bundled dependencies (no manual install needed): - `@stellar/stellar-sdk` — core (transaction building, Soroban RPC, contract spec) - `@stellar/freighter-api` — Freighter connector - `@albedo-link/intent` — Albedo connector - `@creit.tech/xbull-wallet-connect` — xBull connector - `@ledgerhq/hw-app-str` + `@ledgerhq/hw-transport-webhid` + `@ledgerhq/hw-transport-webusb` — Ledger connector - `@walletconnect/sign-client` — WalletConnect connector - `qr-code-styling` (in `@saganta/stellar-appkit-ui-web`) — styled QR codes for the WalletConnect pairing view - `motion` (in `@saganta/stellar-appkit-ui-web`) — animation presets for modal/bottom-sheet transitions Framework wrappers are optional peer dependencies (install the one for your framework): - `react` + `react-dom` — `@saganta/stellar-appkit/react` - `vue` — `@saganta/stellar-appkit/vue` - `solid-js` — `@saganta/stellar-appkit/solid` - `svelte` — `@saganta/stellar-appkit/svelte` Frameworks must remain as peer deps because your app already has its own framework instance — two copies of React would break hooks. The wallet SDKs don't have this singleton constraint, so they're safe to bundle. ## Quick Start ```ts import { StellarAppKit } from '@saganta/stellar-appkit'; import '@saganta/stellar-appkit-ui-web'; // connectors is optional — defaults to Freighter, Albedo, xBull, Ledger // domain + uri are optional too — auto-derived from window.location in the browser const appkit = new StellarAppKit({ network: 'TESTNET', appMetadata: { name: 'Example App' }, }); const modal = document.querySelector('stellar-appkit-modal'); modal.client = appkit; modal.open(); await appkit.restore(); ``` Zero-config: - `connectors` — omit to auto-register Freighter + Albedo + xBull + Ledger. Use `defaultConnectors()` to extend with WalletConnect (requires `projectId`). - `appMetadata` follows WC metadata standard: `{ name, description?, url?, icons? }`. Same object fed to WalletConnect. `domain` for SIWS derived from `url`, `uri` = `url`. Auto-derived from `window.location.origin` when omitted. ### WalletConnect (Hana, Lobstr, Hot Wallet) ```ts import { StellarAppKit, createWalletConnectConnector, defaultConnectors, Networks, } from '@saganta/stellar-appkit'; const appkit = new StellarAppKit({ network: 'TESTNET', connectors: [ ...defaultConnectors(), createWalletConnectConnector({ projectId: 'your-wc-cloud-project-id', metadata: { name: 'My App', description: '...', url: '...', icons: [] }, networkPassphrase: Networks.TESTNET, // onUri is OPTIONAL — the modal renders the QR code automatically }), ], }); ``` WalletConnect covers every non-extension Stellar wallet — Hana (SDF's wallet), Lobstr, Hot Wallet, and any mobile wallet. - **With `` (recommended):** the QR code is rendered automatically as an inline SVG via [`qr-code-styling`](https://www.npmjs.com/package/qr-code-styling). The styling matches Reown's QR aesthetic: rounded data modules (`dotsOptions.type: 'rounded'`), extra-rounded finder pattern outer rings (`cornersSquareOptions.type: 'extra-rounded'`), and circular finder pattern inner dots (`cornersDotOptions.type: 'dot'`). Omit `onUri` entirely. - **Without the modal (custom UI):** set `onUri` to render the QR yourself (use `qr-code-styling`, `qrcode.react`, or `qrcode` — `better-qr` was used in older versions and is no longer bundled). - **`Networks` export:** `import { Networks } from '@saganta/stellar-appkit'` — no need for `@stellar/stellar-sdk`. - `@walletconnect/sign-client` is bundled (lazy-imported, tree-shaken if unused); `qr-code-styling` is a dependency of `@saganta/stellar-appkit-ui-web` (lazy-imported inside the modal only when the WC connector is registered). - **WalletConnect is pinned to the top of the wallet list** when present (it's the only "always-available" connector since it's a relay, not a browser extension). Its sub-label reads "Scan QR Code" instead of "Installed". ## Wallet Connection ```ts // Connect await appkit.connect('freighter'); // Session appkit.session; // ConnectSession | null appkit.activeConnector; // WalletConnector | null // Events appkit.on('connect', (session) => {}); appkit.on('disconnect', ({ walletId }) => {}); appkit.on('sessionsChanged', (sessions) => {}); appkit.on('error', (err) => {}); // Network mismatch recovery await appkit.connect('freighter', { autoRetryNetworkMismatch: true }); ``` ## Signing ```ts // Transaction (goes through preview flow automatically) const result = await appkit.signTransaction(xdr); const result = await appkit.signTransaction(xdr, { skipPreview: true }); // Message const result = await appkit.signMessage(message); // SIWS (Sign-In With Stellar) const { message, signedMessage, signerAddress, signedData } = await appkit.signIn({ statement: 'Sign in to Example App', nonce: 'server-issued-nonce', }); // Automatic SIWS — set `siws` on the StellarAppKit config and the modal // auto-triggers sign-in after wallet connect. Full session lifecycle: // siws: { statement, signoutOnDisconnect, disconnectOnFail, maxRetries, timeoutMs, // session: async () => SiwsSession | null, // check existing // nonce: async () => string, // verify: async (data, nonce, ctx: {address, network}) => SiwsSession | null, // signout: async () => boolean, // refresh: async () => SiwsSession | null, // optional // } // API: appkit.siwsSession, signOut(), requireAuth(), validateSession(), reauthenticate() // Hooks: useSiwsSession(), useIsAuthenticated() // Event: siwsSessionChange // Auth entry (Soroban delegated auth) const result = await appkit.signAuthEntry(preimageBase64); ``` ## Soroban ```ts import { SorobanConnection } from '@saganta/stellar-appkit'; const soroban = new SorobanConnection({ rpcUrl: 'https://soroban-testnet.stellar.org', // OR multi-provider failover: rpcUrls: ['https://soroban-testnet.stellar.org', 'https://rpc-backup.example.com'], networkPassphrase: Networks.TESTNET, wallet: appkit, }); // Full pipeline: build → simulate → prepare → sign → submit → poll const result = await soroban.invoke({ contractId, method: 'transfer', args }); // Preview with balance deltas + fee estimate const preview = await soroban.previewInvoke({ contractId, method: 'transfer', args }); // preview.balanceDeltas, preview.feeEstimate, preview.simulationStatus // Fee estimation const fee = await soroban.estimateFee(unsignedXdr); // Typed contract client const token = soroban.contract('C...', { specEntries }); await token.transfer({ from, to, amount: 100n }); const balance = await token.simulate('balanceOf', { id }); // RPC failover status soroban.getFailoverStatus(); ``` ## SIWS Verification (server-side) ```ts import { verifySiws } from '@saganta/stellar-appkit-siws-verify'; const result = await verifySiws( { message, signedMessage, signerAddress, signedData }, { expectedDomain: 'app.example.com', expectedNonce, debug: true } ); if (result.ok) { console.log(result.claims.address); } else { console.log(result.reason); console.log(result.diagnostics); // when debug: true } ``` ## Framework Wrappers ### React ```tsx import { StellarAppKitProvider, useConnect, useSession, useSoroban } from '@saganta/stellar-appkit-ui-web/react'; const { connect, isConnected } = useConnect(); const session = useSession(); const { invoke, soroban } = useSoroban({ rpcUrl, networkPassphrase }); ``` ### Vue ```ts import { StellarAppKitPlugin, useConnect, useSession } from '@saganta/stellar-appkit-ui-web/vue'; app.use(StellarAppKitPlugin, { network: 'TESTNET', connectors: [...] }); // In setup(): const { connect } = useConnect(); const session = useSession(); ``` ### Solid ```tsx import { StellarAppKitProvider, useConnect, useSession } from '@saganta/stellar-appkit-ui-web/solid'; ``` ### Svelte ```svelte import { setStellarAppKitContext, useConnect, useSession } from '@saganta/stellar-appkit-ui-web/svelte'; setStellarAppKitContext({ network: 'TESTNET', connectors: [...] }); const { connect } = useConnect(); const session = useSession(); // $isConnected, $session?.address ``` ## Framework Modal Components Each wrapper ships a typed component wrapping the `` Web Component. Always import `@saganta/stellar-appkit/ui-web` once at app entry to register the custom element — the framework wrappers don't import it themselves (keeps them SSR-safe). ### React ```tsx import { useRef } from 'react'; import { StellarAppKitProvider, StellarAppKitModal } from '@saganta/stellar-appkit-ui-web/react'; import type { StellarAppKitModalHandle } from '@saganta/stellar-appkit-ui-web/react'; import '@saganta/stellar-appkit-ui-web'; function ModalHost() { const ref = useRef(null); return ( <> console.log(s)} /> ); } ``` ### Vue ```vue ``` ### Solid ```tsx import { StellarAppKitModal } from '@saganta/stellar-appkit-ui-web/solid'; import type { StellarAppKitModalHandle } from '@saganta/stellar-appkit-ui-web/solid'; import '@saganta/stellar-appkit-ui-web'; let handle: StellarAppKitModalHandle | undefined; (handle = h)} mode="auto" theme="minimal" />; ; ``` ### Svelte (use:stellarmodal action) ```svelte console.log(e.detail)} /> ``` ### Modal props (all frameworks) - `mode`: 'auto' | 'modal' | 'bottomsheet' | 'inline' - `theme`: a named theme (`'minimal'` (default), `'stellar'`, `'sky'`, `'ocean'`, `'sunset'`) with optional `:light` suffix (e.g. `'sky:light'`), `'auto'` (follows `prefers-color-scheme`), or `'dark'` / `'light'` (backwards-compat → `minimalDark` / `minimalLight`). See "Theming" below. - `branding`: 'default' | 'minimal' | 'hidden' - `logoSrc`: string (URL) - `title`: string - `autoRetryNetwork`: boolean - `stellarExpertAvatars`: boolean - `animation`: 'none' | 'fade' | 'scale' | 'scale-blur' | 'slide-up' | 'slide-left' | 'implode' — preset for both open and close (default: 'scale-blur' for modal, 'slide-up' for bottomsheet) - `animationOpen`: same preset names — overrides `animation` for the open transition only - `animationClose`: same preset names — overrides `animation` for the close transition only Animation config priority: HTML attributes (`animation-open` / `animation-close`) > `animation` attribute > `StellarAppKit` config (`modal.animation`) > mode-based default. All animations are zero-dependency WAAPI and respect `prefers-reduced-motion`. ### Modal events (all frameworks) - `connect` — fired when a wallet connects (mirrors client `connect` event) - `disconnect` — fired when a wallet disconnects - `error` — fired on client errors (rejected, network mismatch, etc.) ### Modal imperative handle (React/Solid/Vue via ref) - `open()` — opens the modal (no-op in inline mode) - `close(skipAnimation?: boolean)` — closes the modal; pass `true` to skip the WAAPI exit animation - `element` — the underlying `` DOM node (escape hatch) ### Svelte helpers - `stellarmodal(node)` — `use:stellarmodal` action, wires up the client - `openModal(node)` / `closeModal(node)` — imperative open/close - `isStellarAppKitModal(node)` — type guard for narrowing ## Transaction Preview ```ts const appkit = new StellarAppKit({ network: 'PUBLIC', connectors: [...], previewOptions: { verifiedContracts: new Set(['CA...KNOWN']), largeTransferThreshold: 1000, contractMetadata: new Map([ ['CBETT2CX...', { name: 'USDC', verified: true, audited: true, auditUrl: '...' }], ]), includeFeeEstimate: true, }, onPreviewTransaction: async (preview) => { console.log(preview.operations, preview.riskFlags, preview.feeEstimate); return true; }, onPreviewAuthEntry: async (preview) => { console.log(preview.authorizedContracts); return true; }, }); ``` ## Theming The modal ships with **5 named themes**, each with a dark and light variant (10 theme objects total). Themes are built on a shared **zinc neutral base palette** (`#09090B` / `#FFFFFF` bg, `#18181B` / `#F8F8F8` surface, `#FAFAFA` / `#18181B` text). Each named theme only overrides the accent color — the rest of the palette stays consistent across themes. | Theme | Accent (dark / light) | Notes | |---|---|---| | `minimal` (default) | `#FAFAFA` / `#18181B` (neutral) | No brand color — fits any project | | `stellar` | `#6EE7B7` / `#0E9A6E` (Stellar green) | Stellar brand theme | | `sky` | `#38BDF8` / `#0EA5E9` (light sky blue) | Open, friendly | | `ocean` | `#60A5FA` / `#1D4ED8` (deep ocean blue) | Serious, financial | | `sunset` | `#FB7185` / `#E11D48` (warm coral/pink) | Energetic, creative | The `theme` attribute accepts: - A named theme: `theme="minimal"`, `theme="stellar"`, `theme="sky"`, `theme="ocean"`, `theme="sunset"` (resolves to the dark variant by default) - Named theme with `:light` suffix: `theme="sky:light"`, `theme="ocean:light"` - `theme="auto"` — follows `prefers-color-scheme` (resolves to `minimal` dark/light) - `theme="dark"` or `theme="light"` (backwards compat — both map to `minimalDark` / `minimalLight`) - Omitted / unknown — defaults to `minimal` (dark) ```css stellar-appkit-modal { --sak-color-bg: #09090B; --sak-color-surface: #18181B; --sak-color-accent: #6EE7B7; /* override the theme's accent */ --sak-color-text: #FAFAFA; --sak-radius-lg: 20px; --sak-font-display: 'Geist Sans', sans-serif; } ``` ```html ``` Resolved theme objects are also exported from `@saganta/stellar-appkit-ui-web` for non-UI use: `minimalDark`, `minimalLight`, `stellarDark`, `stellarLight`, `skyDark`, `skyLight`, `oceanDark`, `oceanLight`, `sunsetDark`, `sunsetLight`, plus `THEME_NAMES` and `THEME_REGISTRY`. ## Built-in wallet UX features These features are wired into ``'s connected view — no config needed: - **"Get Testnet funds" button** — only rendered when `session.network === 'TESTNET'`. Calls `https://friendbot.stellar.org/?addr=
` via `fetch()` (no `window.open` — the user stays in the modal). Shows a "Funding requested…" banner, then polls the balance at 3s / 6s / 10s after the click so the new XLM appears automatically. Testnet-only — Futurenet / Standalone / Public don't render the button. - **Balance + transaction-history polling** — while the modal is open and a wallet is connected, the modal silently refreshes the balance + recent transactions every **10 seconds** (silent = no skeleton-loading flash; cached values stay visible until the new fetch resolves). Polling starts on modal open, stops on close/disconnect. After any successful `signTransaction()`, the modal also refreshes the balance + history 2 seconds later so the new transaction shows up immediately. - **WalletConnect pinned to top of wallet list** — when a WalletConnect connector is registered, it sorts to the top of the wallet list and its sub-label reads "Scan QR Code" instead of "Installed". - **Ledger icon** — the Ledger wallet row uses a bundled inline SVG: white background with a black "L" wordmark at `font-weight: 400` (Inter / Helvetica Neue / Helvetica / Arial). ## Error Handling ```ts import { ConnectError, NetworkMismatchError } from '@saganta/stellar-appkit'; try { await appkit.connect('freighter'); } catch (err) { if (err instanceof NetworkMismatchError) { console.log(err.expectedNetwork, err.actualNetwork); } if (err instanceof ConnectError) { console.log(err.code); // -1, -2, -3, -4 } } ``` ## Tree-shaking Each connector and framework wrapper is a separate subpath export: - `@saganta/stellar-appkit` — core (no UI code) - `@saganta/stellar-appkit/ui-web` — modal Web Component - `@saganta/stellar-appkit/react` — React hooks - `@saganta/stellar-appkit/vue` — Vue composables - `@saganta/stellar-appkit/solid` — Solid hooks - `@saganta/stellar-appkit/svelte` — Svelte stores A React app never ships Vue/Svelte/Solid code. ## Links - [GitHub](https://github.com/sagantaHQ/stellar-appkit) - [Documentation](https://stellar-appkit.saganta.com) - [Live Demos](https://demos.stellar-appkit.saganta.com) - [npm: @saganta/stellar-appkit](https://www.npmjs.com/package/@saganta/stellar-appkit) - [npm: @saganta/stellar-appkit-siws-verify](https://www.npmjs.com/package/@saganta/stellar-appkit-siws-verify) - [License: MIT](https://github.com/sagantaHQ/stellar-appkit/blob/main/LICENSE)