@cointracker/tax-kit
v2.3.0
Published
CoinTracker SDK for supplying cost basis data
Readme
@cointracker/tax-kit
CoinTracker SDK for supplying cost basis data and integrating tax calculation features into your application.
Installation
npm install @cointracker/tax-kitUsage
import { TaxKitProvider, useTaxKit } from '@cointracker/tax-kit';
// Wrap your app with the TaxKitProvider
function App() {
const fetchAccessToken = async () => {
// Fetch access token from your backend
const response = await fetch('/api/user-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
return data.access_token;
};
return (
<TaxKitProvider fetchAccessToken={fetchAccessToken}>
<YourApp />
</TaxKitProvider>
);
}
// Use the useTaxKit hook in your components
function YourComponent() {
const {
open,
isSyncingTransactions,
lastSyncUpdatedTimestamp,
error,
numberOfConnections,
syncingConnections,
isCalculatingCostBasis,
disconnectUser,
isUserConnected,
} = useTaxKit();
return (
<div>
<button onClick={open}>Connect CoinTracker</button>
{isSyncingTransactions && <p>Syncing transactions...</p>}
{syncingConnections && <p>Syncing connections...</p>}
{isCalculatingCostBasis && <p>Calculating cost basis...</p>}
{isUserConnected && (
<div>
<p>Connected with {numberOfConnections} connection(s)</p>
<button onClick={disconnectUser}>Disconnect</button>
</div>
)}
{error && <p>Error: {error.message}</p>}
</div>
);
}Hook API Reference
The useTaxKit hook returns the following properties:
open- Function to open the CoinTracker modal for the user to connect their accountisTaxKitOpen- Boolean indicating if the TaxKit modal is currently openisTaxKitLoading- Boolean indicating if the Tax Kit is currently loading (computing status)isSyncingTransactions- Boolean indicating if the SDK is currently syncing transactions to the partnerlastSyncUpdatedTimestamp- Timestamp (ms since epoch) of the last sync update, or null if never syncederror- Any error encountered during sync or authentication (TaxKitError | null)numberOfConnections- The number of connections the user has configured (number | null)syncingConnections- Boolean indicating if the SDK is currently syncing connectionsisCalculatingCostBasis- Boolean indicating if the SDK is currently calculating cost basistaxKitStatus- Current TaxKit synchronization status with title, description, and action label (TaxKitStatusInfo | null)disconnectUser- Function that disconnects the user and resets tax kit to empty state. Returns a PromiseisUserConnected- Boolean indicating if the user has started the tax kit flow
TaxKit Status Values
The taxKitStatus object contains a status field with one of these values:
GET_STARTED- User needs to connect accounts and start auto-syncSYNCED- Cost basis has successfully been sent to the partnerTRANSACTIONS_NEED_REVIEW- Transactions require user review before proceedingUNABLE_TO_SYNC_TO_PARTNER- Sync to partner failed due to an errorUPGRADE_PLAN_NEEDED- User has reached their plan limit and needs to upgradeREADY_TO_IMPORT- Tax lots are ready to be imported. Awaiting user confirmation.COST_BASIS_FAILED- Cost basis calculation has failedFIX_API_CONNECTIONS- API connections need to be fixedIMPORTING_COST_BASIS- Sending tax lots to the partnerSYNCING_WALLETS_AND_CALCULATING_COST_BASIS- Syncing wallets and calculating cost basisERROR- Error in the tax kit (e.g. authentication error or generic error)
Configuration
TaxKitProvider groups configuration into three optional sub-objects: theme, options, and metadata. Callbacks (onOpenLink, onDisconnectUser, onPartnerImportTransactionsSuccess, onReceivedAdditionalMetadata) and copy stay at the top level.
Theme
Pass color tokens and dark-mode preference under theme:
<TaxKitProvider
fetchAccessToken={fetchAccessToken}
theme={{
mode: 'dark', // optional — defaults to the user's OS preference
light: {
radius: '0.5rem',
spacing: '0.25rem',
fontSans: '"Inter", sans-serif',
background: 'rgb(255, 255, 255)',
foreground: 'rgb(10, 11, 13)',
primary: 'rgb(0, 82, 255)',
// ...see ThemeContract for all available tokens
},
dark: {
radius: '0.5rem',
spacing: '0.25rem',
fontSans: '"Inter", sans-serif',
background: 'rgb(10, 11, 13)',
foreground: 'rgb(245, 248, 255)',
primary: 'rgb(55, 115, 245)',
},
}}
>
<YourApp />
</TaxKitProvider>Tip: memoize
theme(or itslight/darksub-objects) withuseMemoif your parent component re-renders frequently — this avoids redundantCONFIGmessages to the iframe.
Partner branding (logo and fonts)
theme accepts three optional fields for partners that want their own logo and brand font inside the iframe.
theme.partnerLogo
Renders your logo in the iframe header in place of the SDK's built-in mark. Accepts either a single string URL (used in both modes) or a per-mode object:
theme={{
// Single URL — same logo in light and dark mode
partnerLogo: 'https://cdn.your-partner.com/logo.svg',
}}theme={{
// Separate logos so a dark logo can be used on light backgrounds, and vice versa
partnerLogo: {
light: 'https://cdn.your-partner.com/logo-dark-on-light.svg',
dark: 'https://cdn.your-partner.com/logo-light-on-dark.svg',
},
}}Notes:
- The image is hosted by you — point at a CORS-accessible URL on your CDN or origin. Prefer SVG; if you ship PNG, keep it small (≤ 128px square). Visual size is locked to the iframe header — there's no risk of oversized images breaking layout — but bandwidth is still on you.
- Alt text is auto-derived from the partner slug (e.g.
"Coinbase logo"); you do not need to pass it. - When
partnerLogois unset, the SDK uses its built-in logo for known partner slugs (coinbase,kraken).
theme.fonts
Self-hosted @font-face definitions for woff2/woff/ttf/otf files you serve from your own CDN. Each entry is injected verbatim, so you control format, weight, style, and font-display.
theme={{
fonts: [
{
family: 'Partner Sans',
src: 'https://cdn.your-partner.com/partner-sans-regular.woff2',
weight: '400',
style: 'normal',
},
{
family: 'Partner Sans',
src: 'https://cdn.your-partner.com/partner-sans-bold.woff2',
weight: '700',
style: 'normal',
},
],
light: { fontSans: '"Partner Sans", sans-serif', /* ...other tokens */ },
dark: { fontSans: '"Partner Sans", sans-serif', /* ...other tokens */ },
}}theme.fontStylesheets
External font stylesheet URL(s) — Google Fonts, Adobe Fonts, Bunny Fonts, etc. The iframe injects each one as <link rel="stylesheet">, so the provider handles @font-face rules, unicode-range subsetting, and format negotiation for you.
theme={{
// Single URL
fontStylesheets:
'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap',
}}theme={{
// Multiple URLs
fontStylesheets: [
'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap',
'https://use.typekit.net/your-kit-id.css',
],
light: { fontSans: '"Inter", sans-serif', /* ... */ },
}}Use fonts for self-hosted brand fonts where you control the source URLs, fontStylesheets for off-the-shelf web fonts. The two are independent — use either, both, or neither.
Options
Behavior switches (environment, flow, overlay style, UI toggles) live under options:
<TaxKitProvider
fetchAccessToken={fetchAccessToken}
options={{
mode: 'production', // 'production' | 'alpha' | 'mock'
flow: 'one-way', // 'one-way' | 'continuous'
overlayMode: 'responsive', // 'drawer' | 'dialog' | 'responsive'
defaultOpen: false,
helpLinkUrl: 'https://help.<your-partner>.com/tax',
}}
>
<YourApp />
</TaxKitProvider>Custom Metadata
The SDK supports passing custom metadata between the parent application and the SDK iframe. This is useful for partner-specific data that needs to be quickly implemented without requiring updates to the Tax Kit SDK with new dedicated props.
Passing Metadata to SDK
<TaxKitProvider
fetchAccessToken={fetchAccessToken}
metadata={{
customField: 'any-value',
}}
>
<YourApp />
</TaxKitProvider>Reserved Keys
One key inside metadata has special handling by the SDK:
promoCode(string) — applied as the user's promo code.
All other keys are partner-defined and pass through opaquely to the iframe.
<TaxKitProvider
fetchAccessToken={fetchAccessToken}
metadata={{
promoCode: 'PARTNER_2026',
// arbitrary partner extras are allowed alongside reserved keys
customField: 'any-value',
}}
>
<YourApp />
</TaxKitProvider>Receiving Metadata from SDK
The SDK can send metadata back to the parent application using the onReceivedAdditionalMetadata callback:
<TaxKitProvider
fetchAccessToken={fetchAccessToken}
onReceivedAdditionalMetadata={(metadata) => {
console.log('Received from SDK:', metadata);
// Handle partner-specific events or data
}}
>
<YourApp />
</TaxKitProvider>The metadata is typed as Record<string, unknown> allowing flexible partner-specific implementations without SDK changes.
URL Parameter Metadata
The SDK also supports passing metadata via URL parameters on the partner application. Add a cointracker-tax-kit-param query parameter to your page URL, and the SDK will automatically include it in the metadata:
https://your-partner-site.com/tax-center?cointracker-tax-kit-param=my-valueFeatures
- Transaction management and tracking
- Integration with various exchanges and wallets
- Cost basis calculation
- Tax form generation support
Error Codes
TaxKit provides error codes that can be accessed through the error property returned by the useTaxKit hook. These errors help you handle different failure scenarios in your application.
General TaxKit Errors
These errors are available through the TaxKitError enum and represent high-level SDK state errors:
generic_error- A generic error occurredmissing_connections- No connections are configured for the userconnection_sync_errors- One or more connections have sync errorstransactions_needs_review- Transactions require manual reviewauthentication_error- Authentication failed (e.g., invalid or expired access token)
Example Usage
import { useTaxKit, TaxKitError } from '@cointracker/tax-kit';
function YourComponent() {
const { error } = useTaxKit();
if (error) {
// Check error codes using the TaxKitError enum
switch (error) {
case TaxKitError.MissingConnections:
return <p>Please connect an exchange or wallet to get started.</p>;
case TaxKitError.AuthenticationError:
return <p>Authentication failed. Please try again.</p>;
case TaxKitError.ConnectionSyncErrors:
return (
<p>
Some connections have sync errors. Please check your connections.
</p>
);
case TaxKitError.TransactionsNeedsReview:
return (
<p>Some transactions require review before they can be processed.</p>
);
default:
return <p>An error occurred: {error}</p>;
}
}
return <div>TaxKit is ready</div>;
}Migrating from the flat prop shape
In version 2.2.0 the flat prop shape was reorganized into grouped sub-objects. The old props still work and are mapped onto the new shape internally, but each one logs a console.warn once per browser session and will be removed in a future major version.
| Old prop | New location |
| ------------------- | ---------------------------- |
| apiBaseUrl | options.apiBaseUrl |
| hostPageUrl | options.hostPageUrl |
| mode | options.mode |
| flow | options.flow |
| overlayMode | options.overlayMode |
| defaultOpen | options.defaultOpen |
| hideNavigationBar | options.hideNavigationBar |
| hideBackButton | options.hideBackButton |
| helpLinkUrl | options.helpLinkUrl |
| themeContract | theme.light / theme.dark |
| themeMode | theme.mode |
| userPromoCode | metadata.promoCode |
When both an old and new prop are passed, the new shape wins. Pass null to metadata.promoCode to explicitly clear a value rather than fall back to the legacy prop.
Removed in 2.2.0: partnerCostBasisMethodsByYear prop
The partnerCostBasisMethodsByYear prop and its iframe-side machinery were removed entirely. The dedicated Record<number, CostBasisMethod> pipe had been built speculatively for per-year cost-basis enforcement, but no UI component ever consumed it. Partners (Coinbase confirmed in writing) pass cost-basis info via metadata.costBasisMethodInfo instead — a different key that the iframe actually reads (CostBasisMethodMismatchWarningOverlay.tsx). Partners who need to pass cost-basis-related metadata should continue using metadata.costBasisMethodInfo or other custom keys.
Removed in 2.2.0: startRoute prop and ?startRoute= URL forwarding
Both the startRoute prop and the ?startRoute= URL forwarding were removed. Investigation showed the feature was effectively a no-op in production: canonical state-driven routing in the iframe overrode startRoute in all but one specific case (/checkout-complete, which itself isn't reached via URL today — the iframe transitions to it via internal xstate events after Stripe payment). Partners who passed startRoute were paying for a feature that wasn't actually navigating users anywhere.
If a real deep-linking need surfaces post-merge, we'll design and ship it as a proper feature.
Removed in 2.2.0: themeClassName prop
The themeClassName prop was removed. The iframe's wrapper now derives its class directly from theme.mode — partners no longer need to think about Tailwind custom variants or pass any className.
Why the change is safe:
- The iframe SDK's bundled CSS has zero Tailwind utilities using the
ct-embedded-dark:*orpro:*custom variants — those variant declarations existed but never matched any utility class, so values like'ct-embedded-dark'or'pro dark'were doing nothing meaningful inside the iframe. - The functionally important piece — providing a
.darkancestor so base-ui'sdark:*Tailwind utilities can fire — is now handled by applyingtheme.mode(e.g.'dark') directly to the iframe's wrapper. - The parent app's own theme className mechanism (which IS load-bearing for parent-side
pro:*utilities underapps/embedded-api/app/) is completely separate and unaffected by this change.
Partners who previously passed themeClassName should remove it. No replacement is needed — theme.mode covers the only thing the prop actually did inside the iframe.
Removed in 2.2.0: overridePartnerLogo prop
The overridePartnerLogo prop was removed. The iframe now resolves the partner logo directly from the JWT-derived partner identity (embeddedHealth.partnerId) via the iframe's icon registry. Partners no longer need to pass a separate logo override — if the JWT identifies your integration as 'kraken', 'coinbase', etc., the matching logo is rendered automatically.
Updated in 2.3.0: if you're a new partner and don't have a hardcoded SVG in the iframe's registry, pass your logo URL directly via theme.partnerLogo — no CoinTracker-side code change needed. The hardcoded registry still acts as the fallback for production partners (Coinbase, Kraken).
License
ISC - Copyright CoinTracker
