packages/addon-sdk/README.md
A comprehensive TypeScript SDK for building secure, feature-rich addons for Wealthfolio. Extend your portfolio management experience with custom analytics, integrations, and visualizations.
Get up and running with your first addon in minutes:
# 1. Create a new project
mkdir my-portfolio-addon && cd my-portfolio-addon
# 2. Initialize and install dependencies
npm init -y
npm install @wealthfolio/addon-sdk react react-dom
npm install -D typescript @types/react vite @vitejs/plugin-react
# 3. Create basic files
echo '{"id": "my-addon", "name": "My Portfolio Addon", "version": "1.0.0"}' > manifest.json
mkdir src && touch src/index.ts
# 4. Start building your addon!
// src/index.ts
import { getAddonContext, type AddonContext } from '@wealthfolio/addon-sdk';
export default function enable(context: AddonContext) {
// Add navigation item
const navItem = context.sidebar.addItem({
id: 'my-addon',
label: 'My Addon',
icon: 'chart-line',
route: '/addons/my-addon',
});
// Register route
context.router.add({
path: '/addons/my-addon',
component: () => import('./MyComponent'),
});
// Log activation
context.api.logger.info('My addon activated!');
// Cleanup on disable
context.onDisable(() => {
navItem.remove();
context.api.logger.info('My addon deactivated');
});
}
# Using npm
npm install @wealthfolio/addon-sdk @tanstack/react-query
# Using yarn
yarn add @wealthfolio/addon-sdk @tanstack/react-query
# Using pnpm
pnpm add @wealthfolio/addon-sdk @tanstack/react-query
@wealthfolio/addon-sdkThe SDK supports multiple import patterns:
// Default import (recommended)
import { getAddonContext } from '@wealthfolio/addon-sdk';
// Named imports
import { AddonContext, PermissionLevel } from '@wealthfolio/addon-sdk';
// Type-only imports
import type { AddonManifest, Permission } from '@wealthfolio/addon-sdk';
// Subpath imports
import type { PortfolioHolding } from '@wealthfolio/addon-sdk/types';
import { PERMISSION_CATEGORIES } from '@wealthfolio/addon-sdk/permissions';
Create your addon with the following recommended structure:
my-portfolio-addon/
βββ manifest.json # Addon metadata and permissions
βββ src/
β βββ index.ts # Main entry point
β βββ components/ # React components
β β βββ Dashboard.tsx
β βββ hooks/ # Custom hooks
β βββ types/ # TypeScript types
β βββ utils/ # Utility functions
βββ dist/ # Built output
β βββ addon.js
βββ assets/ # Static assets
βββ package.json
βββ tsconfig.json
βββ vite.config.ts # Build configuration
Create a manifest.json file in your addon root:
{
"id": "investment-fees-tracker",
"name": "Investment Fees Tracker",
"version": "1.0.0",
"description": "Track and analyze investment fees across your portfolio",
"author": "Your Name",
"homepage": "https://github.com/yourname/investment-fees-tracker",
"license": "MIT",
"main": "dist/addon.js",
"sdkVersion": "1.0.0",
"minWealthfolioVersion": "1.0.0",
"keywords": ["portfolio", "fees", "tracking", "analytics"],
"icon": "data:image/svg+xml;base64,...",
"permissions": [
{
"category": "portfolio",
"functions": ["getHoldings"],
"purpose": "Access portfolio data to calculate fee analytics"
},
{
"category": "activities",
"functions": ["getAll"],
"purpose": "Analyze transaction history for fee calculations"
}
]
}
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (lowercase, hyphens allowed) |
name | string | Human-readable addon name |
version | string | Semantic version (e.g., "1.0.0") |
| Field | Type | Description |
|---|---|---|
description | string | Brief description of functionality |
author | string | Author name or organization |
homepage | string | Project homepage URL |
license | string | License identifier |
main | string | Entry point file (default: "addon.js") |
sdkVersion | string | Compatible SDK version |
permissions | Permission[] | Security permissions required |
minWealthfolioVersion | string | Minimum Wealthfolio version required |
keywords | string[] | Keywords for discoverability |
icon | string | Addon icon (base64 or relative path) |
Based on the current SDK architecture, here's a complete real-world addon example:
// src/addon.tsx
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk';
import { Icons } from '@wealthfolio/ui';
import FeesPage from './pages/fees-page';
// Main addon component
function InvestmentFeesTrackerAddon({ ctx }: { ctx: AddonContext }) {
return (
<div className="investment-fees-tracker-addon">
<FeesPage ctx={ctx} />
</div>
);
}
// Addon enable function - called when the addon is loaded
const enable: AddonEnableFunction = (context) => {
context.api.logger.info('π° Investment Fees Tracker addon is being enabled!');
// Store references to items for cleanup
const addedItems: Array<{ remove: () => void }> = [];
try {
// Add sidebar navigation item with icon from UI library
const sidebarItem = context.sidebar.addItem({
id: 'investment-fees-tracker',
label: 'Fee Tracker',
icon: <Icons.Invoice className="h-5 w-5" />,
route: '/addons/investment-fees-tracker',
order: 200
});
addedItems.push(sidebarItem);
context.api.logger.debug('Sidebar navigation item added successfully');
// Create wrapper component with shared QueryClient
const InvestmentFeesTrackerWrapper = () => {
const sharedQueryClient = context.api.query.getClient();
return (
<QueryClientProvider client={sharedQueryClient}>
<InvestmentFeesTrackerAddon ctx={context} />
</QueryClientProvider>
);
};
// Register route with lazy loading
context.router.add({
path: '/addons/investment-fees-tracker',
component: React.lazy(() => Promise.resolve({
default: InvestmentFeesTrackerWrapper
}))
});
context.api.logger.debug('Route registered successfully');
context.api.logger.info('Investment Fees Tracker addon enabled successfully');
} catch (error) {
context.api.logger.error('Failed to initialize addon: ' + (error as Error).message);
throw error; // Re-throw so addon system can handle it
}
// Register cleanup callback
context.onDisable(() => {
context.api.logger.info('π Investment Fees Tracker addon is being disabled');
// Remove all sidebar items
addedItems.forEach(item => {
try {
item.remove();
} catch (error) {
context.api.logger.error('Error removing sidebar item: ' + (error as Error).message);
}
});
context.api.logger.info('Investment Fees Tracker addon disabled successfully');
});
};
// Export the enable function as default
export default enable;
context.api.query.getClient() for consistent
data fetching@wealthfolio/ui for consistent iconography
### Advanced Component Example
```typescript
// components/FeesPage.tsx
import React, { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { AddonContext } from '@wealthfolio/addon-sdk';
import type { Holding, Account, Activity } from '@wealthfolio/addon-sdk/types';
interface FeesPageProps {
ctx: AddonContext;
}
export function FeesPage({ ctx }: FeesPageProps) {
// Use React Query for data fetching with the shared client
const { data: accounts, isLoading: accountsLoading } = useQuery({
queryKey: ['accounts'],
queryFn: () => ctx.api.accounts.getAll()
});
const { data: holdings, isLoading: holdingsLoading } = useQuery({
queryKey: ['holdings'],
queryFn: async () => {
if (!accounts || accounts.length === 0) return [];
// Get holdings for all accounts
const allHoldings = await Promise.all(
accounts.map(account => ctx.api.portfolio.getHoldings(account.id))
);
return allHoldings.flat();
},
enabled: !!accounts && accounts.length > 0
});
const { data: activities, isLoading: activitiesLoading } = useQuery({
queryKey: ['activities'],
queryFn: () => ctx.api.activities.getAll()
});
const isLoading = accountsLoading || holdingsLoading || activitiesLoading;
// Calculate total fees from activities
const totalFees = React.useMemo(() => {
if (!activities?.data) return 0;
return activities.data.reduce((total, activity) => {
// Look for fee-related activities or transaction costs
const fee = activity.fee || 0;
return total + fee;
}, 0);
}, [activities]);
useEffect(() => {
if (!isLoading) {
ctx.api.logger.info('Fees data loaded successfully', {
accountsCount: accounts?.length,
holdingsCount: holdings?.length,
activitiesCount: activities?.data?.length,
totalFees
});
}
}, [isLoading, accounts, holdings, activities, totalFees, ctx.api.logger]);
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p>Loading fees data...</p>
</div>
</div>
);
}
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Investment Fees Tracker</h1>
<p className="text-gray-600">Track and analyze fees across your investment portfolio</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow border">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Total Fees Paid</h3>
<p className="text-3xl font-bold text-red-600">
${totalFees.toLocaleString('en-US', { minimumFractionDigits: 2 })}
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow border">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Accounts Tracked</h3>
<p className="text-3xl font-bold text-blue-600">{accounts?.length || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow border">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Holdings</h3>
<p className="text-3xl font-bold text-green-600">{holdings?.length || 0}</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow border">
<h2 className="text-xl font-semibold mb-4">Recent Fee Activities</h2>
<div className="space-y-3">
{activities?.data?.slice(0, 5).map((activity) => (
<div key={activity.id} className="flex justify-between items-center py-2 border-b">
<div>
<p className="font-medium">{activity.activityType}</p>
<p className="text-sm text-gray-600">{activity.date}</p>
</div>
<span className="text-red-600 font-medium">
${(activity.fee || 0).toFixed(2)}
</span>
</div>
))}
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow border">
<h2 className="text-xl font-semibold mb-4">Account Summary</h2>
<div className="space-y-3">
{accounts?.map((account) => (
<div key={account.id} className="flex justify-between items-center py-2 border-b">
<div>
<p className="font-medium">{account.name}</p>
<p className="text-sm text-gray-600">{account.accountType}</p>
</div>
<span className="text-gray-900 font-medium">
${account.balance?.toLocaleString('en-US', { minimumFractionDigits: 2 }) || '0.00'}
</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}
export default FeesPage;
<h2 className="text-lg font-semibold mb-4">Holdings Overview</h2>
<p>Total holdings: {holdings.length}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h2 className="text-lg font-semibold mb-4">Account Summary</h2>
<p>Total accounts: {accounts.length}</p>
</div>
</div>
</div>
);
}
export default AnalyticsDashboard;
// hooks/usePortfolioData.ts
import { useState, useEffect } from 'react';
import { getAddonContext } from '@wealthfolio/addon-sdk';
import type { Holding, PerformanceMetrics } from '@wealthfolio/addon-sdk/types';
export function usePortfolioData(accountId?: string) {
const [holdings, setHoldings] = useState<Holding[]>([]);
const [performance, setPerformance] = useState<PerformanceMetrics | null>(
null,
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
setError(null);
const ctx = getAddonContext();
const holdingsData = await ctx.api.portfolio.getHoldings(
accountId || '',
);
setHoldings(holdingsData);
if (accountId) {
const performanceData =
await ctx.api.portfolio.calculatePerformanceSummary({
itemType: 'account',
itemId: accountId,
});
setPerformance(performanceData);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
fetchData();
}, [accountId]);
return { holdings, performance, loading, error };
}
| Category | Risk Level | Description |
|---|---|---|
ui | Low | Add navigation items and routes |
market-data | Low | Access market prices and quotes |
events | Low | Listen to application events |
currency | Low | Access exchange rates |
portfolio | Medium | Access holdings and valuations |
files | Medium | File dialog operations |
financial-planning | Medium | Goals and contribution limits |
activities | High | Transaction history access |
accounts | High | Account management |
settings | High | Application configuration |
{
"permissions": [
{
"category": "portfolio",
"functions": ["getHoldings", "getHolding", "calculatePerformanceSummary"],
"purpose": "Display detailed portfolio analytics and performance metrics"
},
{
"category": "activities",
"functions": ["getAll", "create"],
"purpose": "Access transaction history for fee calculations and analysis"
},
{
"category": "market-data",
"functions": ["searchTicker", "getQuoteHistory"],
"purpose": "Show price charts and enable ticker search functionality"
}
]
}
Create a vite.config.ts for optimal bundling:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyPortfolioAddon',
fileName: 'addon',
formats: ['es'],
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
outDir: 'dist',
minify: 'terser',
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
});
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
# Install dependencies
npm install
# Build for production
npm run build
# The built addon will be in dist/addon.js
# Create a ZIP package with all necessary files
zip -r my-portfolio-addon.zip \
manifest.json \
dist/ \
assets/ \
README.md
Your final package should contain:
manifest.json - Addon metadatadist/addon.js - Compiled addon codeassets/ - Static assets (optional)README.md - Documentation (optional)For development, you can test addons locally:
# Build in watch mode
npm run dev
# Your changes will be reflected after reloading addons in Wealthfolio
sidebar.addItem(config)Add an item to the application sidebar.
Parameters:
config.id (string): Unique identifierconfig.label (string): Display textconfig.icon (string | ReactNode): Icon name or componentconfig.route (string): Navigation routeconfig.order (number): Display order (optional)config.onClick (function): Click handler (optional)Returns: SidebarItemHandle with remove() method
router.add(route)Register a new route in the application.
Parameters:
route.path (string): Route path patternroute.component (LazyExoticComponent): Lazy-loaded componentonDisable(callback)Register cleanup callback for addon disable.
Parameters:
callback (function): Cleanup functionAll data access is performed through the context's api property:
const ctx = getAddonContext();
// Portfolio data
const holdings = await ctx.api.portfolio.getHoldings(accountId);
const accounts = await ctx.api.accounts.getAll();
// Market data
const quotes = await ctx.api.marketData.getQuoteHistory(symbol);
const profile = await ctx.api.marketData.getAssetProfile(assetId);
// Financial planning
const goals = await ctx.api.goals.getAll();
const limits = await ctx.api.financialPlanning.getContributionLimit();
// Settings
const settings = await ctx.api.getSettings();
// Logging and debugging
ctx.api.logger.info('Operation completed successfully');
ctx.api.logger.error('Error occurred:', error);
ctx.api.logger.debug('Debug info:', debugData);
| Method | Description | Permission Required |
|---|---|---|
portfolio.getHoldings(accountId) | Get portfolio holdings for account | portfolio |
portfolio.getHolding(accountId, assetId) | Get specific holding | portfolio |
portfolio.calculatePerformanceSummary(params) | Calculate performance metrics | portfolio |
portfolio.getIncomeSummary() | Get income summary data | portfolio |
accounts.getAll() | Get all account information | accounts |
accounts.create(account) | Create new account | accounts |
activities.getAll(accountId?) | Get activity history (optionally filtered to one account) | activities |
activities.create(activity) | Create new activity | activities |
marketData.getQuoteHistory(symbol) | Get historical quotes | market-data |
marketData.getAssetProfile(assetId) | Get asset profile | market-data |
marketData.searchTicker(query) | Search for tickers | market-data |
goals.getAll() | Get financial goals | financial-planning |
goals.getFunding(goalId) | Get funding rules for a goal | financial-planning |
goals.saveFunding(goalId, rules) | Save funding rules for a goal | financial-planning |
settings.get() | Get app settings | settings |
query.getClient() | Get shared QueryClient instance | None |
Tip:
activities.getAllaccepts an optional account ID string to scope results to a single account. The SDK normalizes this for both desktop (Tauri) and web runtimesβno need to wrap it in an array.
activities.search accepts either a single value or an array for accountIds
and activityTypes. The host normalizes these inputs for both desktop and web
runtime paths and will also accept an explicit symbol filter when you want to
target a single ticker without a free-form search query. Sorting takes a single
sort object and defaults to { id: "date", desc: true } when none is provided.
The first two parameters are pagination controls: page is a zero-based index
(use 0 for the first page) and pageSize is the number of rows to return. For
exports you can pass a large pageSize (for example, 1000) alongside page = 0
to fetch a wide slice in one call.
const response = await ctx.api.activities.search(
0,
50,
{
accountIds: 'account-1', // single string or string[] both work
activityTypes: ['BUY', 'DIVIDEND'],
symbol: 'AAPL',
},
'', // optional keyword search (ignored when empty)
{ id: 'date', desc: true },
);
The SDK provides a comprehensive logging system:
const ctx = getAddonContext();
// Log levels: 'error', 'warn', 'info', 'debug'
ctx.api.logger.error('Critical error occurred', { error, context });
ctx.api.logger.warn('Warning message', additionalData);
ctx.api.logger.info('Information message');
ctx.api.logger.debug('Debug information', debugObject);
// Set log level (for development)
ctx.api.logger.setLevel('debug');
// Check if logging level is enabled
if (ctx.api.logger.isLevelEnabled('debug')) {
ctx.api.logger.debug('Expensive debug operation', expensiveData);
}
The SDK provides access to Wealthfolio's shared React Query client for consistent data fetching and caching:
// Access the shared QueryClient instance
const sharedQueryClient = context.api.query.getClient();
// Wrap your components with QueryClientProvider
const MyAddonWrapper = () => {
return (
<QueryClientProvider client={sharedQueryClient}>
<MyAddonComponent />
</QueryClientProvider>
);
};
// Use React Query hooks in your components
function MyAddonComponent() {
const { data: accounts, isLoading } = useQuery({
queryKey: ['accounts'],
queryFn: () => ctx.api.accounts.getAll()
});
const { data: holdings } = useQuery({
queryKey: ['holdings', selectedAccountId],
queryFn: () => ctx.api.portfolio.getHoldings(selectedAccountId),
enabled: !!selectedAccountId
});
// Your component logic here
}
Benefits of Shared QueryClient:
// Before
import ctx from '@wealthfolio/addon-sdk';
// After (recommended)
import { getAddonContext } from '@wealthfolio/addon-sdk';
const ctx = getAddonContext();
// Before
import type { AddonContext, AddonManifest } from '@wealthfolio/addon-sdk';
// After (more specific)
import type { AddonContext } from '@wealthfolio/addon-sdk';
import type { AddonManifest } from '@wealthfolio/addon-sdk/manifest';
# Create a new directory for your addon
mkdir my-portfolio-addon
cd my-portfolio-addon
# Initialize package.json
npm init -y
# Install the SDK and peer dependencies
npm install @wealthfolio/addon-sdk
npm install --save-dev typescript @types/react vite @vitejs/plugin-react
# Install React (peer dependency)
npm install react react-dom
npm install --save-dev @types/react-dom
Create the essential configuration files:
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyPortfolioAddon',
fileName: 'addon',
formats: ['es'],
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
outDir: 'dist',
minify: 'terser',
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
});
package.json scripts
{
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"type-check": "tsc --noEmit",
"package": "npm run build && zip -r addon.zip manifest.json dist/ assets/ README.md"
}
}
# Start development mode (watches for changes)
npm run dev
# Type checking
npm run type-check
# Build for production
npm run build
# Create distribution package
npm run package
If you want to contribute to the SDK itself:
# Clone the Wealthfolio repository
git clone https://github.com/afadil/wealthfolio.git
cd wealthfolio/packages/addon-sdk
# Install dependencies
pnpm install
# Build the SDK
pnpm build
# Watch for changes during development
pnpm dev
The SDK uses tsup for building with the following configuration:
// tsup.config.ts
export default defineConfig({
entry: {
index: 'src/index.ts',
types: 'src/types.ts',
permissions: 'src/permissions.ts',
},
format: ['esm'],
dts: true, // Generate TypeScript declarations
clean: true, // Clean dist folder before build
sourcemap: true, // Generate source maps
minify: false, // Keep code readable for debugging
target: 'es2020',
external: ['react'], // Don't bundle React
});
# Build the SDK
pnpm build
# Link for local testing
npm link
# In your addon project
npm link @wealthfolio/addon-sdk
# Test your changes
npm run dev
The SDK is published to the npm registry. For maintainers:
# Ensure you're logged in to npm
npm login
# Update version in package.json
npm version patch # or minor/major
# Build and publish
npm run build
npm publish
# Or for beta releases
npm publish --tag beta
// In your addon
const ctx = getAddonContext();
ctx.api.logger.setLevel('debug');
ctx.api.logger.debug('Debug information:', data);
Access the browser's developer console for debugging:
During development, enable hot reloading:
// Add to your addon's main file
if (process.env.NODE_ENV === 'development') {
// Enable hot module replacement
if (module.hot) {
module.hot.accept();
}
}
import { getAddonContext } from '@wealthfolio/addon-sdk';
async function fetchPortfolioData() {
const ctx = getAddonContext();
try {
// Get all accounts first, then holdings for each
const accounts = await ctx.api.accounts.getAll();
const holdings = await Promise.all(
accounts.map((account) => ctx.api.portfolio.getHoldings(account.id)),
).then((results) => results.flat());
return holdings;
} catch (error) {
ctx.api.logger.error('Failed to fetch holdings:', error);
// Handle different error types
if (error.code === 'PERMISSION_DENIED') {
// Show permission error to user
} else if (error.code === 'NETWORK_ERROR') {
// Handle network issues
}
throw error;
}
}
export default function enable(context: AddonContext) {
const subscriptions: (() => void)[] = [];
// Add event listeners
const unsubscribe = context.events.subscribe('portfolio.updated', handler);
subscriptions.push(unsubscribe);
// Cleanup on disable
context.onDisable(() => {
subscriptions.forEach((unsub) => unsub());
context.api.logger.info('Addon cleaned up successfully');
});
}
// Use React state for component-level state
const [loading, setLoading] = useState(false);
const [data, setData] = useState<PortfolioData | null>(null);
// Use context API for global addon state
const AddonStateContext = createContext<AddonState | null>(null);
export function AddonProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AddonState>(initialState);
return (
<AddonStateContext.Provider value={{ state, setState }}>
{children}
</AddonStateContext.Provider>
);
}
// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
// Use React.Suspense
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart data={chartData} />
</Suspense>
// Use React Query or SWR for caching
import { useQuery } from 'react-query';
function usePortfolioData(accountId: string) {
return useQuery(
['portfolio', accountId],
() => ctx.api.portfolio.getHoldings(accountId),
{
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
},
);
}
// vite.config.ts - optimize chunks
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
charts: ['chart.js', 'd3'],
},
},
},
},
});
We welcome contributions to improve the addon SDK!
Fork and Clone
git clone https://github.com/yourusername/wealthfolio.git
cd wealthfolio/packages/addon-sdk
Install Dependencies
pnpm install
Make Changes
# Start development mode
pnpm dev
# Run type checking
pnpm lint
# Build for testing
pnpm build
Testing Your Changes
# Link the SDK locally for testing
npm link
# In your test addon project
npm link @wealthfolio/addon-sdk
Submit Changes
| Field | Value |
|---|---|
| Package Name | @wealthfolio/addon-sdk |
| Scope | @wealthfolio |
| Registry | npmjs.com |
| License | MIT |
| Repository | GitHub |
We follow Semantic Versioning (SemVer):
| SDK Version | Wealthfolio Version | Node.js | React |
|---|---|---|---|
| 1.0.x | >= 1.0.0 | >= 18.0.0 | ^18.0.0 |
| 0.9.x | >= 0.9.0 | >= 16.0.0 | ^17.0.0 |
# Latest stable version
npm install @wealthfolio/addon-sdk
# Specific version
npm install @wealthfolio/[email protected]
# Version range
npm install @wealthfolio/addon-sdk@^1.0.0
# Latest beta version
npm install @wealthfolio/addon-sdk@beta
# Specific beta version
npm install @wealthfolio/[email protected]
# Install directly from GitHub
npm install github:afadil/wealthfolio#main
# Or from a specific branch/commit
npm install github:afladil/wealthfolio#wealthfolio-addons
# View package information
npm info @wealthfolio/addon-sdk
# View all available versions
npm view @wealthfolio/addon-sdk versions --json
# View latest version
npm view @wealthfolio/addon-sdk version
# View package dependencies
npm view @wealthfolio/addon-sdk dependencies
# Check for outdated packages
npm outdated @wealthfolio/addon-sdk
# Login to npm (maintainers only)
npm login
# Verify login
npm whoami
# Check publishing permissions
npm access list packages @wealthfolio
# 1. Update version
npm version patch # or minor/major
# 2. Build the package
npm run build
# 3. Test the build
npm pack
tar -tf wealthfolio-addon-sdk-*.tgz
# 4. Publish to npm
npm publish
# 5. For beta releases
npm publish --tag beta
# 6. Tag the release
git tag v$(node -p "require('./package.json').version")
git push --tags
| Tag | Purpose | Command |
|---|---|---|
latest | Stable releases | npm publish |
beta | Beta releases | npm publish --tag beta |
alpha | Alpha releases | npm publish --tag alpha |
next | Next major version | npm publish --tag next |
View package statistics:
# Check for vulnerabilities
npm audit
# Fix vulnerabilities
npm audit fix
# View security advisories
npm audit --audit-level=moderate
# Verify package integrity
npm pack --dry-run
# Check package contents
npm pack && tar -tf *.tgz
MIT - see LICENSE for details.
Error: Cannot resolve module '@wealthfolio/addon-sdk'
Solutions:
# Clear npm cache
npm cache clean --force
# Delete node_modules and reinstall
rm -rf node_modules package-lock.json
npm install
# Check Node.js version (requires >= 18.0.0)
node --version
Error: Cannot find type definitions
Solutions:
// Ensure proper TypeScript configuration
{
"compilerOptions": {
"moduleResolution": "bundler", // or "node"
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
}
// Use explicit type imports
import type { AddonContext } from '@wealthfolio/addon-sdk';
Error: React version mismatch
Solutions:
# Install correct React version
npm install react@^18.0.0 react-dom@^18.0.0
# Check installed versions
npm list react react-dom
Error: Vite build fails with external dependencies
Solutions:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
external: ['react', 'react-dom', '@wealthfolio/addon-sdk'],
},
},
});
Error: Permission denied for API call
Solutions:
// Add required permissions to manifest.json
{
"permissions": [
{
"category": "portfolio",
"functions": ["holdings"],
"purpose": "Access portfolio data for analytics"
}
]
}
Error: getAddonContext() returns undefined
Solutions:
// Ensure you're calling it within addon context
function MyComponent() {
useEffect(() => {
// Call context inside useEffect or event handlers
const ctx = getAddonContext();
// ... use context
}, []);
}
// Don't call at module level
// const ctx = getAddonContext(); // β Wrong
# Ensure dev mode is enabled
npm run dev
# Check if files are being watched
ls -la dist/ # Should update when you save files
Check the addon package structure:
addon.zip
βββ manifest.json β
βββ dist/
β βββ addon.js β
βββ assets/ (optional)
Validate manifest.json:
# Check JSON syntax
cat manifest.json | jq .
Check Wealthfolio logs:
// Add error handling and logging
try {
const accounts = await ctx.api.accounts.getAll();
const data = await ctx.api.portfolio.getHoldings(accounts[0]?.id);
ctx.api.logger.info('Data loaded successfully', { count: data.length });
} catch (error) {
ctx.api.logger.error('API call failed', {
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
});
}
// Use code splitting and lazy loading
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// Reduce bundle size
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns'],
},
},
},
},
});
// Proper cleanup in useEffect
useEffect(() => {
const subscription = ctx.events.subscribe('update', handler);
return () => {
subscription.unsubscribe(); // β Clean up
};
}, []);
// Cleanup on addon disable
context.onDisable(() => {
// Clean up all resources
clearInterval(intervalId);
subscription.unsubscribe();
});
If you're still experiencing issues:
Check Version Compatibility:
npm list @wealthfolio/addon-sdk
Create Minimal Reproduction:
Search Existing Issues:
Provide Complete Information: