docs/addons/addon-migration-guide-v2-to-v3.md
This guide covers all changes needed to migrate your Wealthfolio addon from v2 to v3.
| Aspect | v2 | v3 |
|---|---|---|
| SDK Version | 2.0.0 | 3.0.0 |
| React Version | 19.1.1 | 19.2.4 |
| Route Prefix | /addon/ | /addons/ |
Change the sdkVersion in your manifest:
// v2
{
"sdkVersion": "2.0.0"
}
// v3
{
"sdkVersion": "3.0.0"
}
Also update your addon version:
{
"version": "2.0.0"
}
Update the @wealthfolio/addon-sdk dependency and devDependencies:
// v2
{
"peerDependencies": {
"react": "^19.1.1",
"react-dom": "^19.1.1"
}
}
// v3
{
"peerDependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
}
}
// v2
{
"devDependencies": {
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^20.0.0",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.1",
"rollup-plugin-external-globals": "^0.13.0",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"vite": "^7.1.5"
}
}
// v3
{
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^20.19.33",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.3.4",
"rollup-plugin-external-globals": "^0.13.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0"
}
}
Key changes:
^4.1.13 to ^4.1.18@vitejs/plugin-react (required for React support)clsx and tailwind-merge (commonly used utilities)@types/react* versions^7.3.1Wealthfolio v3 uses Tailwind CSS v4. While the addon system uses the host app's Tailwind configuration, there are some important considerations:
v4 uses the new @import syntax:
/* v2/v3 - Use this syntax */
@import "tailwindcss";
In Tailwind v4, you don't need a tailwind.config.js file. The configuration is
done via CSS:
/* globals.css in the host app */
@theme {
--color-primary: #your-color;
--font-sans: "Inter", sans-serif;
}
v4 uses a new dark mode variant syntax:
/* v4 syntax */
@custom-variant dark (&:where(.dark, .dark *));
In v4, custom utilities are defined using @utility:
@utility scrollbar-hide {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
Addons should use the @wealthfolio/ui package which provides pre-built
components styled with the host app's theme. The UI package exports components
that automatically inherit the host app's:
primary, secondary, etc.)// Your addon can use UI components directly
import { Button, Card, Page } from "@wealthfolio/ui";
Addons can use all standard Tailwind utility classes. The host app provides a custom theme via CSS variables that your addon classes will automatically inherit:
// These classes will automatically use the host app's theme
function MyComponent() {
return (
<div className="bg-background text-foreground p-4 rounded-lg border-border">
<h1 className="text-primary font-semibold">Title</h1>
</div>
);
}
The sync function now uses assetIds instead of symbols:
// v2
await ctx.api.market.sync(["AAPL", "MSFT"], true);
// v3
await ctx.api.market.sync(["asset-id-1", "asset-id-2"], true);
// or with recent days refetch
await ctx.api.market.sync(["asset-id-1"], true, 7);
The return type changed from QuoteSummary[] to SymbolSearchResult[]:
// v2
const results = await ctx.api.market.searchTicker("AAPL");
// QuoteSummary[]
// v3
const results = await ctx.api.market.searchTicker("AAPL");
// SymbolSearchResult[] - has additional fields: exchangeMic, currency, assetKind, isExisting, existingAssetId
updateDataSource replaced with updateQuoteMode:
// v2
await ctx.api.assets.updateDataSource("AAPL", "MANUAL");
// v3
await ctx.api.assets.updateQuoteMode("asset-id", "MANUAL");
Methods now use assetId instead of symbol:
// v2
await ctx.api.quotes.update('AAPL', { close: 150.00, ... });
const history = await ctx.api.quotes.getHistory('AAPL');
// v3
await ctx.api.quotes.update('asset-id', { close: 150.00, ... });
const history = await ctx.api.quotes.getHistory('asset-id');
The activity types now include additional types and use string values for quantities:
// v2 - 13 types
const ActivityType = {
BUY: "BUY",
SELL: "SELL",
DIVIDEND: "DIVIDEND",
INTEREST: "INTEREST",
DEPOSIT: "DEPOSIT",
WITHDRAWAL: "WITHDRAWAL",
ADD_HOLDING: "ADD_HOLDING",
REMOVE_HOLDING: "REMOVE_HOLDING",
TRANSFER_IN: "TRANSFER_IN",
TRANSFER_OUT: "TRANSFER_OUT",
FEE: "FEE",
TAX: "TAX",
SPLIT: "SPLIT",
};
// v3 - 15 types (added CREDIT, ADJUSTMENT, UNKNOWN)
const ActivityType = {
BUY: "BUY",
SELL: "SELL",
SPLIT: "SPLIT",
DIVIDEND: "DIVIDEND",
INTEREST: "INTEREST",
DEPOSIT: "DEPOSIT",
WITHDRAWAL: "WITHDRAWAL",
TRANSFER_IN: "TRANSFER_IN",
TRANSFER_OUT: "TRANSFER_OUT",
FEE: "FEE",
TAX: "TAX",
CREDIT: "CREDIT",
ADJUSTMENT: "ADJUSTMENT",
UNKNOWN: "UNKNOWN",
};
Key changes to the Activity interface:
// v2
interface Activity {
id: string;
type: ActivityType;
date: Date | string;
quantity: number;
unitPrice: number;
currency: string;
fee: number;
isDraft: boolean;
comment?: string;
accountId?: string | null;
symbolProfileId: string;
}
// v3 - uses strings for precision, has activityType + activityTypeOverride
interface Activity {
id: string;
accountId: string;
assetId?: string; // NOW OPTIONAL for pure cash events
activityType: string; // Canonical type (closed set of 15)
activityTypeOverride?: string; // User override
sourceType?: string; // Raw provider label
subtype?: string; // Semantic variation (DRIP, STAKING_REWARD, etc.)
status: ActivityStatus;
activityDate: string; // ISO timestamp (UTC)
quantity?: string; // STRING to preserve precision
unitPrice?: string;
amount?: string;
fee?: string;
currency: string;
fxRate?: string;
isUserModified: boolean;
needsReview: boolean;
// ... more fields
}
The Asset interface has been redesigned around identity:
// v2 - uses symbol as primary identity
interface Asset {
id: string;
symbol: string;
name?: string;
assetType?: string;
// ...
}
// v3 - identity is opaque (UUID), classification via kind and instrumentType
interface Asset {
id: string;
// Classification
kind: AssetKind; // INVESTMENT, PROPERTY, VEHICLE, COLLECTIBLE, etc.
name?: string;
displayCode?: string; // User-visible ticker/label
// Valuation
quoteMode: QuoteMode; // MARKET or MANUAL
quoteCcy: string;
// Instrument identity (null for non-market assets)
instrumentType?: string; // EQUITY, CRYPTO, FX, OPTION, METAL
instrumentSymbol?: string; // Canonical symbol (AAPL, BTC, EUR)
instrumentExchangeMic?: string;
// ...
}
// v3 - adds assetKind
interface Holding {
id: string;
holdingType: HoldingType;
accountId: string;
instrument?: Instrument | null;
assetKind?: AssetKind | null; // NEW in v3
// ... rest same
}
v3 introduces several new types:
// AssetKind - how the asset behaves
const AssetKind = {
INVESTMENT: "INVESTMENT",
PROPERTY: "PROPERTY",
VEHICLE: "VEHICLE",
COLLECTIBLE: "COLLECTIBLE",
PRECIOUS_METAL: "PRECIOUS_METAL",
PRIVATE_EQUITY: "PRIVATE_EQUITY",
LIABILITY: "LIABILITY",
OTHER: "OTHER",
FX: "FX",
};
// QuoteMode - how price is determined
const QuoteMode = {
MARKET: "MARKET",
MANUAL: "MANUAL",
};
// ActivityStatus
const ActivityStatus = {
POSTED: "POSTED",
PENDING: "PENDING",
DRAFT: "DRAFT",
VOID: "VOID",
};
// ActivitySubtypes
const ACTIVITY_SUBTYPES = {
DRIP: "DRIP",
QUALIFIED: "QUALIFIED",
ORDINARY: "ORDINARY",
STAKING_REWARD: "STAKING_REWARD",
LENDING_INTEREST: "LENDING_INTEREST",
COUPON: "COUPON",
// ... more subtypes
};
Routes now use /addons/ prefix instead of /addon/:
// v2
ctx.router.add({
path: "/addon/my-addon",
component: React.lazy(() => import("./MyPage")),
});
// v3
ctx.router.add({
path: "/addons/my-addon",
component: React.lazy(() => import("./MyPage")),
});
The permission categories may have changed. Review your addon permissions and ensure they still match the v3 categories. The general structure remains the same:
{
"permissions": [
{
"category": "activities",
"functions": ["search", "create", "update"],
"purpose": "Access and manage trading activities"
}
]
}
Here's a complete example of migrating an addon from v2 to v3:
// src/addon.tsx
import { type AddonContext } from "@wealthfolio/addon-sdk";
export default function enable(ctx: AddonContext) {
ctx.api.logger.info("My addon enabled!");
// Add sidebar
const sidebarItem = ctx.sidebar.addItem({
id: "my-addon",
label: "My Addon",
route: "/addon/my-addon", // old route prefix
order: 150,
});
// Register route
ctx.router.add({
path: "/addon/my-addon",
component: React.lazy(() => import("./pages/MainPage")),
});
// Fetch holdings (v2 style)
const holdings = await ctx.api.portfolio.getHoldings(accountId);
// Update quote (v2 style)
await ctx.api.quotes.update(symbol, { close: 150.0 });
ctx.onDisable(() => {
sidebarItem.remove();
});
}
// src/addon.tsx
import { type AddonContext } from "@wealthfolio/addon-sdk";
export default function enable(ctx: AddonContext) {
ctx.api.logger.info("My addon enabled!");
// Add sidebar
const sidebarItem = ctx.sidebar.addItem({
id: "my-addon",
label: "My Addon",
route: "/addons/my-addon", // new route prefix
order: 150,
});
// Register route
ctx.router.add({
path: "/addons/my-addon",
component: React.lazy(() => import("./pages/MainPage")),
});
// Fetch holdings - same API
const holdings = await ctx.api.portfolio.getHoldings(accountId);
// Update quote (v3 style - uses assetId)
await ctx.api.quotes.update(assetId, { close: 150.0 });
ctx.onDisable(() => {
sidebarItem.remove();
});
}
VITE_ENABLE_ADDON_DEV_MODE=true pnpm tauri devpnpm dev:server/addons/ prefixSolution: Use string types for quantity fields (quantity?: string) and
check for activityTypeOverride
Solution: Verify route uses /addons/ prefix instead of /addon/
Solution: Use assetId instead of symbol for quotes and market data
operations
addons/ directory