@wealthfolio/addon-sdk 3.6.2 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,8 @@ integrations, and visualizations.
42
42
  - **ESM Support**: Modern ECMAScript modules with tree-shaking support
43
43
  - **Comprehensive Logging**: Built-in logging system with multiple levels
44
44
  - **Event System**: Subscribe to application events and state changes
45
+ - **Spend Categorization**: Manage reusable expense, income, and savings rules
46
+ - **Localization**: Follow the host locale with scoped addon translations
45
47
  - **Performance Optimized**: Lightweight bundle with minimal overhead
46
48
  - **Developer Tools**: Built-in debugging and development utilities
47
49
  - **Backwards Compatible**: Stable API with semantic versioning
@@ -70,12 +72,15 @@ mkdir src && touch src/index.ts
70
72
 
71
73
  ```typescript
72
74
  // src/index.ts
73
- import { createRoot, type Root } from 'react-dom/client';
74
- import { getAddonContext, type AddonContext } from '@wealthfolio/addon-sdk';
75
+ import type { AddonContext } from '@wealthfolio/addon-sdk';
75
76
  import { MyComponent } from './MyComponent';
76
77
 
78
+ let addonContext: AddonContext | undefined;
79
+
80
+ const MyAddonRoute = () => <MyComponent ctx={addonContext!} />;
81
+
77
82
  export default function enable(context: AddonContext) {
78
- let root: Root | null = null;
83
+ addonContext = context;
79
84
 
80
85
  // Add navigation item
81
86
  const navItem = context.sidebar.addItem({
@@ -87,11 +92,9 @@ export default function enable(context: AddonContext) {
87
92
 
88
93
  // Register route
89
94
  context.router.add({
95
+ id: 'my-addon',
90
96
  path: '/addons/my-addon',
91
- render: ({ root: routeRoot }) => {
92
- root ??= createRoot(routeRoot);
93
- root.render(<MyComponent ctx={context} />);
94
- },
97
+ component: MyAddonRoute,
95
98
  });
96
99
 
97
100
  // Log activation
@@ -99,8 +102,7 @@ export default function enable(context: AddonContext) {
99
102
 
100
103
  // Cleanup on disable
101
104
  context.onDisable(() => {
102
- root?.unmount();
103
- root = null;
105
+ addonContext = undefined;
104
106
  navItem.remove();
105
107
  context.api.logger.info('My addon deactivated');
106
108
  });
@@ -122,15 +124,15 @@ pnpm add @wealthfolio/addon-sdk @tanstack/react-query
122
124
 
123
125
  ### Requirements
124
126
 
125
- - **Node.js**: >= 18.0.0
126
- - **React**: ^18.0.0 (peer dependency)
127
+ - **Node.js**: >= 20.0.0
128
+ - **React**: ^19.2.4 (peer dependency and host-provided version)
127
129
  - **TypeScript**: ^5.0.0 (recommended for development)
128
- - **React Query**: ^4.0.0 or ^5.0.0 (for data fetching)
130
+ - **React Query**: ^5.90.0 (for data fetching)
129
131
 
130
132
  ### Package Information
131
133
 
132
134
  - **Package Name**: `@wealthfolio/addon-sdk`
133
- - **Current Version**: 1.0.0
135
+ - **Current Version**: 3.8.0
134
136
  - **Bundle Format**: ESM (ECMAScript Modules)
135
137
  - **Type Definitions**: Included (TypeScript ready)
136
138
  - **License**: MIT
@@ -140,23 +142,28 @@ pnpm add @wealthfolio/addon-sdk @tanstack/react-query
140
142
 
141
143
  ### Import Methods
142
144
 
143
- The SDK supports multiple import patterns:
145
+ The SDK supports a public entry point plus focused subpath imports:
144
146
 
145
147
  ```typescript
146
- // Default import (recommended)
147
- import { getAddonContext } from '@wealthfolio/addon-sdk';
148
-
149
- // Named imports
150
- import { AddonContext, PermissionLevel } from '@wealthfolio/addon-sdk';
151
-
152
- // Type-only imports
153
- import type { AddonManifest, Permission } from '@wealthfolio/addon-sdk';
154
-
155
- // Subpath imports
156
- import type { PortfolioHolding } from '@wealthfolio/addon-sdk/types';
157
- import { PERMISSION_CATEGORIES } from '@wealthfolio/addon-sdk/permissions';
148
+ // Public entry point (recommended)
149
+ import type {
150
+ AddonContext,
151
+ AddonManifest,
152
+ Holding,
153
+ Permission,
154
+ RiskLevel,
155
+ } from '@wealthfolio/addon-sdk';
156
+ import { PERMISSION_CATEGORIES } from '@wealthfolio/addon-sdk';
157
+
158
+ // Optional module-specific subpath imports
159
+ import type { AddonManifest as Manifest } from '@wealthfolio/addon-sdk/manifest';
160
+ import type { Permission as AddonPermission } from '@wealthfolio/addon-sdk/permissions';
158
161
  ```
159
162
 
163
+ The `/types` subpath contains core context, routing, sidebar, and event types.
164
+ Financial data types such as `Account`, `Activity`, and `Holding` are exported
165
+ from the package root.
166
+
160
167
  ## 🏗️ Project Structure
161
168
 
162
169
  Create your addon with the following recommended structure:
@@ -179,6 +186,46 @@ my-portfolio-addon/
179
186
  └── vite.config.ts # Build configuration
180
187
  ```
181
188
 
189
+ ### Packaged assets
190
+
191
+ Static files below `assets/` and generated files below `dist/assets/` are
192
+ indexed automatically; they do not need to be declared in `manifest.json`.
193
+ JavaScript chunks and CSS in those directories remain runtime code/styles. Load
194
+ other files through the add-on context so the host can keep the opaque iframe
195
+ offline:
196
+
197
+ This API requires Wealthfolio 3.7 or newer. Set `sdkVersion` and
198
+ `minWealthfolioVersion` to `3.7.0` when using it. No permission is required.
199
+
200
+ ```typescript
201
+ export default async function enable(context: AddonContext) {
202
+ const logoUrl = await context.assets.getUrl('assets/logo.png');
203
+ const configBlob = await context.assets.getBlob('assets/config.json');
204
+ const config = JSON.parse(await configBlob.text());
205
+
206
+ // Use logoUrl in an <img>, CSS-in-JS value, or component prop.
207
+ }
208
+ ```
209
+
210
+ The registry also provides `list()` for public path/MIME/size metadata and
211
+ `has(path)` for feature checks. It never exposes host paths or opaque internal
212
+ identifiers. `context.assets` is unrelated to the financial-instrument API at
213
+ `context.api.assets`.
214
+
215
+ Packaged URLs in extracted CSS are resolved automatically and relative to the
216
+ CSS file. For example, `dist/addon.css` can use `url("./assets/background.png")`
217
+ for `dist/assets/background.png`. `data:` and `blob:` URLs remain unchanged. CSS
218
+ `@import` and remote URLs are not supported; bundle imported CSS and use the
219
+ brokered network API for remote data.
220
+
221
+ JavaScript image imports that compile to relative HTTP URLs cannot work in the
222
+ opaque Blob runtime. Use `context.assets.getUrl()` instead. Blob URLs are cached
223
+ for the add-on lifetime and revoked automatically when it is disabled. Package
224
+ limits remain 5 MiB per file, 25 MiB uncompressed in total, and 256 entries.
225
+ Asset roots must be directories; symlinks are rejected. See the
226
+ [v3.6 to v3.7 migration guide](../../docs/addons/addon-migration-guide-v3.6-to-v3.7.md)
227
+ for compatibility and troubleshooting.
228
+
182
229
  ## 📋 Manifest Configuration
183
230
 
184
231
  Create a `manifest.json` file in your addon root:
@@ -193,11 +240,16 @@ Create a `manifest.json` file in your addon root:
193
240
  "homepage": "https://github.com/yourname/investment-fees-tracker",
194
241
  "license": "MIT",
195
242
  "main": "dist/addon.js",
196
- "sdkVersion": "1.0.0",
197
- "minWealthfolioVersion": "1.0.0",
243
+ "sdkVersion": "3.8.0",
244
+ "minWealthfolioVersion": "3.8.0",
198
245
  "keywords": ["portfolio", "fees", "tracking", "analytics"],
199
246
  "icon": "data:image/svg+xml;base64,...",
200
247
  "permissions": [
248
+ {
249
+ "category": "accounts",
250
+ "functions": ["getAll"],
251
+ "purpose": "List accounts whose holdings will be analyzed"
252
+ },
201
253
  {
202
254
  "category": "portfolio",
203
255
  "functions": ["getHoldings"],
@@ -207,6 +259,11 @@ Create a `manifest.json` file in your addon root:
207
259
  "category": "activities",
208
260
  "functions": ["getAll"],
209
261
  "purpose": "Analyze transaction history for fee calculations"
262
+ },
263
+ {
264
+ "category": "performance",
265
+ "functions": ["calculateSummary"],
266
+ "purpose": "Calculate account performance alongside fee totals"
210
267
  }
211
268
  ]
212
269
  }
@@ -233,7 +290,7 @@ Create a `manifest.json` file in your addon root:
233
290
  | `permissions` | `Permission[]` | Security permissions required |
234
291
  | `minWealthfolioVersion` | `string` | Minimum Wealthfolio version required |
235
292
  | `keywords` | `string[]` | Keywords for discoverability |
236
- | `icon` | `string` | Addon icon (base64 or relative path) |
293
+ | `icon` | `string` | Addon icon value supported by the host |
237
294
 
238
295
  ## 🔨 Development Guide
239
296
 
@@ -244,9 +301,12 @@ example:
244
301
 
245
302
  ```typescript
246
303
  // src/addon.tsx
247
- import { createRoot, type Root } from 'react-dom/client';
248
304
  import { QueryClientProvider } from '@tanstack/react-query';
249
- import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk';
305
+ import type {
306
+ AddonContext,
307
+ AddonEnableFunction,
308
+ QueryClient,
309
+ } from '@wealthfolio/addon-sdk';
250
310
  import FeesPage from './pages/fees-page';
251
311
 
252
312
  // Main addon component
@@ -264,7 +324,6 @@ const enable: AddonEnableFunction = (context) => {
264
324
 
265
325
  // Store references to items for cleanup
266
326
  const addedItems: Array<{ remove: () => void }> = [];
267
- let root: Root | null = null;
268
327
 
269
328
  try {
270
329
  // Add sidebar navigation item with a host-supported icon token
@@ -279,11 +338,11 @@ const enable: AddonEnableFunction = (context) => {
279
338
 
280
339
  context.api.logger.debug('Sidebar navigation item added successfully');
281
340
 
282
- // Create wrapper component with shared QueryClient
341
+ // Create wrapper component with this addon's QueryClient
283
342
  const InvestmentFeesTrackerWrapper = () => {
284
- const sharedQueryClient = context.api.query.getClient();
343
+ const addonQueryClient = context.api.query.getClient() as QueryClient;
285
344
  return (
286
- <QueryClientProvider client={sharedQueryClient}>
345
+ <QueryClientProvider client={addonQueryClient}>
287
346
  <InvestmentFeesTrackerAddon ctx={context} />
288
347
  </QueryClientProvider>
289
348
  );
@@ -291,11 +350,9 @@ const enable: AddonEnableFunction = (context) => {
291
350
 
292
351
  // Register route
293
352
  context.router.add({
353
+ id: 'investment-fees-tracker',
294
354
  path: '/addons/investment-fees-tracker',
295
- render: ({ root: routeRoot }) => {
296
- root ??= createRoot(routeRoot);
297
- root.render(<InvestmentFeesTrackerWrapper />);
298
- },
355
+ component: InvestmentFeesTrackerWrapper,
299
356
  });
300
357
 
301
358
  context.api.logger.debug('Route registered successfully');
@@ -310,10 +367,6 @@ const enable: AddonEnableFunction = (context) => {
310
367
  context.onDisable(() => {
311
368
  context.api.logger.info('🛑 Investment Fees Tracker addon is being disabled');
312
369
 
313
- // Unmount the addon's React root
314
- root?.unmount();
315
- root = null;
316
-
317
370
  // Remove all sidebar items
318
371
  addedItems.forEach(item => {
319
372
  try {
@@ -333,32 +386,28 @@ export default enable;
333
386
 
334
387
  ### Key Features Demonstrated
335
388
 
336
- 1. **Shared Query Client**: Uses `context.api.query.getClient()` for consistent
337
- data fetching
338
- 2. **UI Icons**: Leverages `@wealthfolio/ui` for consistent iconography
389
+ 1. **Addon Query Client**: Uses `context.api.query.getClient()` for local data
390
+ fetching with host invalidation bridging
391
+ 2. **UI Icons**: Uses a host-supported icon token for consistent navigation
339
392
  3. **Error Handling**: Comprehensive error handling with logging
340
393
  4. **Resource Management**: Proper cleanup of sidebar items and event listeners
341
394
  5. **TypeScript**: Full type safety with proper imports
342
- 6. **Sandbox Rendering**: Mounts into the route root with `createRoot` and
343
- unmounts on disable
344
-
345
- ````
395
+ 6. **Sandbox Rendering**: Lets the sandbox host own and update the React root
346
396
 
347
397
  ### Advanced Component Example
348
398
 
349
399
  ```typescript
350
400
  // components/FeesPage.tsx
351
- import React, { useEffect, useState } from 'react';
401
+ import React, { useEffect } from 'react';
352
402
  import { useQuery } from '@tanstack/react-query';
353
403
  import type { AddonContext } from '@wealthfolio/addon-sdk';
354
- import type { Holding, Account, Activity } from '@wealthfolio/addon-sdk/types';
355
404
 
356
405
  interface FeesPageProps {
357
406
  ctx: AddonContext;
358
407
  }
359
408
 
360
409
  export function FeesPage({ ctx }: FeesPageProps) {
361
- // Use React Query for data fetching with the shared client
410
+ // Use React Query for data fetching with this addon's client
362
411
  const { data: accounts, isLoading: accountsLoading } = useQuery({
363
412
  queryKey: ['accounts'],
364
413
  queryFn: () => ctx.api.accounts.getAll()
@@ -386,25 +435,20 @@ export function FeesPage({ ctx }: FeesPageProps) {
386
435
 
387
436
  // Calculate total fees from activities
388
437
  const totalFees = React.useMemo(() => {
389
- if (!activities?.data) return 0;
438
+ if (!activities) return 0;
390
439
 
391
- return activities.data.reduce((total, activity) => {
440
+ return activities.reduce((total, activity) => {
392
441
  // Look for fee-related activities or transaction costs
393
- const fee = activity.fee || 0;
442
+ const fee = Number(activity.fee ?? 0);
394
443
  return total + fee;
395
444
  }, 0);
396
445
  }, [activities]);
397
446
 
398
447
  useEffect(() => {
399
448
  if (!isLoading) {
400
- ctx.api.logger.info('Fees data loaded successfully', {
401
- accountsCount: accounts?.length,
402
- holdingsCount: holdings?.length,
403
- activitiesCount: activities?.data?.length,
404
- totalFees
405
- });
449
+ ctx.api.logger.info("Fees data loaded successfully");
406
450
  }
407
- }, [isLoading, accounts, holdings, activities, totalFees, ctx.api.logger]);
451
+ }, [isLoading, ctx.api.logger]);
408
452
 
409
453
  if (isLoading) {
410
454
  return (
@@ -447,14 +491,16 @@ export function FeesPage({ ctx }: FeesPageProps) {
447
491
  <div className="bg-white p-6 rounded-lg shadow border">
448
492
  <h2 className="text-xl font-semibold mb-4">Recent Fee Activities</h2>
449
493
  <div className="space-y-3">
450
- {activities?.data?.slice(0, 5).map((activity) => (
494
+ {activities?.slice(0, 5).map((activity) => (
451
495
  <div key={activity.id} className="flex justify-between items-center py-2 border-b">
452
496
  <div>
453
497
  <p className="font-medium">{activity.activityType}</p>
454
- <p className="text-sm text-gray-600">{activity.date}</p>
498
+ <p className="text-sm text-gray-600">
499
+ {activity.date.toLocaleDateString()}
500
+ </p>
455
501
  </div>
456
502
  <span className="text-red-600 font-medium">
457
- ${(activity.fee || 0).toFixed(2)}
503
+ ${Number(activity.fee ?? 0).toFixed(2)}
458
504
  </span>
459
505
  </div>
460
506
  ))}
@@ -499,17 +545,20 @@ export default FeesPage;
499
545
  }
500
546
 
501
547
  export default AnalyticsDashboard;
502
- ````
548
+ ```
503
549
 
504
550
  ### Using Hooks and State Management
505
551
 
506
552
  ```typescript
507
553
  // hooks/usePortfolioData.ts
508
554
  import { useState, useEffect } from 'react';
509
- import { getAddonContext } from '@wealthfolio/addon-sdk';
510
- import type { Holding, PerformanceResult } from '@wealthfolio/addon-sdk/types';
555
+ import type {
556
+ AddonContext,
557
+ Holding,
558
+ PerformanceResult,
559
+ } from '@wealthfolio/addon-sdk';
511
560
 
512
- export function usePortfolioData(accountId?: string) {
561
+ export function usePortfolioData(ctx: AddonContext, accountId?: string) {
513
562
  const [holdings, setHoldings] = useState<Holding[]>([]);
514
563
  const [performance, setPerformance] = useState<PerformanceResult | null>(
515
564
  null,
@@ -523,26 +572,25 @@ export function usePortfolioData(accountId?: string) {
523
572
  setLoading(true);
524
573
  setError(null);
525
574
 
526
- const ctx = getAddonContext();
575
+ if (!accountId) {
576
+ setHoldings([]);
577
+ setPerformance(null);
578
+ return;
579
+ }
527
580
 
528
- const holdingsData = await ctx.api.portfolio.getHoldings(
529
- accountId || '',
530
- );
581
+ const holdingsData = await ctx.api.portfolio.getHoldings(accountId);
531
582
  setHoldings(holdingsData);
532
583
 
533
- if (accountId) {
534
- const performanceData =
535
- await ctx.api.portfolio.calculatePerformanceSummary({
536
- itemType: 'account',
537
- itemId: accountId,
538
- });
539
- console.log(
540
- performanceData.returns.twr,
541
- performanceData.returns.irr,
542
- performanceData.risk.maxDrawdown,
543
- );
544
- setPerformance(performanceData);
545
- }
584
+ const performanceData = await ctx.api.performance.calculateSummary({
585
+ itemType: 'account',
586
+ itemId: accountId,
587
+ });
588
+ console.log(
589
+ performanceData.returns.twr,
590
+ performanceData.returns.irr,
591
+ performanceData.risk.maxDrawdown,
592
+ );
593
+ setPerformance(performanceData);
546
594
  } catch (err) {
547
595
  setError(err instanceof Error ? err.message : 'Unknown error');
548
596
  } finally {
@@ -551,7 +599,7 @@ export function usePortfolioData(accountId?: string) {
551
599
  }
552
600
 
553
601
  fetchData();
554
- }, [accountId]);
602
+ }, [accountId, ctx]);
555
603
 
556
604
  return { holdings, performance, loading, error };
557
605
  }
@@ -565,18 +613,28 @@ cash flows.
565
613
 
566
614
  ### Permission Categories
567
615
 
568
- | Category | Risk Level | Description |
569
- | -------------------- | ---------- | ------------------------------- |
570
- | `ui` | Low | Add navigation items and routes |
571
- | `market-data` | Low | Access market prices and quotes |
572
- | `events` | Low | Listen to application events |
573
- | `currency` | Low | Access exchange rates |
574
- | `portfolio` | Medium | Access holdings and valuations |
575
- | `files` | Medium | File dialog operations |
576
- | `financial-planning` | Medium | Goals and contribution limits |
577
- | `activities` | High | Transaction history access |
578
- | `accounts` | High | Account management |
579
- | `settings` | High | Application configuration |
616
+ | Category | Risk Level | Description |
617
+ | --------------------- | ---------- | -------------------------------------------- |
618
+ | `market-data` | Low | Search and synchronize market data |
619
+ | `quotes` | Low | Read and update quotes |
620
+ | `events` | Low | Listen to application events |
621
+ | `currency` | Low | Access exchange rates |
622
+ | `assets` | Medium | Read and update financial asset profiles |
623
+ | `performance` | Medium | Calculate portfolio performance |
624
+ | `spending` | Medium | View categories and manage spending rules |
625
+ | `financial-planning` | Medium | Manage goals and allocations |
626
+ | `contribution-limits` | Medium | Manage contribution limits |
627
+ | `files` | Medium | Open host file dialogs |
628
+ | `settings` | Medium | Access application configuration |
629
+ | `portfolio` | High | Access holdings and valuations |
630
+ | `activities` | High | Access and modify transaction history |
631
+ | `accounts` | High | Access and create accounts |
632
+ | `snapshots` | High | Access and modify holdings snapshots |
633
+ | `network` | High | Request declared external HTTPS hosts |
634
+ | `secrets` | High | Store and use secrets through the OS keyring |
635
+
636
+ `ui`, `navigation`, `query`, `toast`, `logger`, and `storage` are baseline
637
+ capabilities and must not be declared as permissions.
580
638
 
581
639
  ### Declaring Permissions
582
640
 
@@ -585,7 +643,7 @@ cash flows.
585
643
  "permissions": [
586
644
  {
587
645
  "category": "portfolio",
588
- "functions": ["getHoldings", "getHolding", "calculatePerformanceSummary"],
646
+ "functions": ["getHoldings", "getHolding"],
589
647
  "purpose": "Display detailed portfolio analytics and performance metrics"
590
648
  },
591
649
  {
@@ -595,8 +653,13 @@ cash flows.
595
653
  },
596
654
  {
597
655
  "category": "market-data",
598
- "functions": ["searchTicker", "getQuoteHistory"],
656
+ "functions": ["searchTicker"],
599
657
  "purpose": "Show price charts and enable ticker search functionality"
658
+ },
659
+ {
660
+ "category": "spending",
661
+ "functions": ["getCategories", "saveRule"],
662
+ "purpose": "Save categorization rules selected by the user"
600
663
  }
601
664
  ]
602
665
  }
@@ -604,6 +667,20 @@ cash flows.
604
667
 
605
668
  ## 🛠️ Build Configuration
606
669
 
670
+ Wealthfolio 3.7 supports Chrome/Edge 107+, Firefox 104+, and Safari 16+. The
671
+ desktop app requires macOS 12+ and the native mobile app requires iOS/iPadOS
672
+ 16+. On macOS 12, apply current macOS and Safari updates so the system WKWebView
673
+ meets the Safari 16 floor. Addons run inside the platform system WebView, so
674
+ build against this browser floor rather than relying on the browser used during
675
+ development.
676
+
677
+ Files below `assets/` and `dist/assets/` are private to the addon package. Use
678
+ `ctx.assets.list()`, `ctx.assets.getBlob(path)`, and `ctx.assets.getUrl(path)`
679
+ to access them. Packaged images, fonts, media, CSS, and WebAssembly are
680
+ supported; Worker and service-worker entry points, popups, direct network
681
+ requests, and remote CSS imports are not. Use the host's brokered APIs,
682
+ including `ctx.api.network.request()`, for declared external access.
683
+
607
684
  ### Vite Configuration
608
685
 
609
686
  Create a `vite.config.ts` for optimal bundling:
@@ -616,6 +693,7 @@ import { resolve } from 'path';
616
693
  export default defineConfig({
617
694
  plugins: [react()],
618
695
  build: {
696
+ target: ['chrome107', 'edge107', 'firefox104', 'safari16'],
619
697
  lib: {
620
698
  entry: resolve(__dirname, 'src/index.ts'),
621
699
  name: 'MyPortfolioAddon',
@@ -750,7 +828,6 @@ Add an item to the application sidebar.
750
828
  `chart-bar`, or `calendar-dots`
751
829
  - `config.route` (string): Navigation route
752
830
  - `config.order` (number): Display order (optional)
753
- - `config.onClick` (function): Click handler (optional)
754
831
 
755
832
  **Returns:** `SidebarItemHandle` with `remove()` method
756
833
 
@@ -761,8 +838,10 @@ Register a new route in the application.
761
838
  **Parameters:**
762
839
 
763
840
  - `route.path` (string): Route path pattern
764
- - `route.render` (function): `({ root, location }) => void` — mount your React
765
- root into the provided `root` element
841
+ - `route.component` (component): Preferred; the host mounts the React component
842
+ and passes the current `location`
843
+ - `route.render` (function): Legacy imperative alternative receiving
844
+ `{ root, location }`
766
845
 
767
846
  #### `onDisable(callback)`
768
847
 
@@ -777,49 +856,72 @@ Register cleanup callback for addon disable.
777
856
  All data access is performed through the context's `api` property:
778
857
 
779
858
  ```typescript
780
- const ctx = getAddonContext();
859
+ // Use the ctx parameter supplied to enable(ctx), or pass it to this helper.
781
860
 
782
861
  // Portfolio data
783
862
  const holdings = await ctx.api.portfolio.getHoldings(accountId);
784
863
  const accounts = await ctx.api.accounts.getAll();
785
864
 
786
865
  // Market data
787
- const quotes = await ctx.api.marketData.getQuoteHistory(symbol);
788
- const profile = await ctx.api.marketData.getAssetProfile(assetId);
866
+ const symbols = await ctx.api.market.searchTicker('AAPL');
867
+ const quotes = await ctx.api.quotes.getHistory(assetId);
868
+ const profile = await ctx.api.assets.getProfile(assetId);
789
869
 
790
870
  // Financial planning
791
871
  const goals = await ctx.api.goals.getAll();
792
- const limits = await ctx.api.financialPlanning.getContributionLimit();
872
+ const limits = await ctx.api.contributionLimits.getAll();
873
+
874
+ // Historical exchange rates and spend categorization (Wealthfolio 3.8+)
875
+ const rates = await ctx.api.exchangeRates.getRatesForDates([
876
+ { fromCurrency: 'USD', toCurrency: 'EUR', date: '2026-09-04' },
877
+ ]);
878
+ const spendCategories = await ctx.api.spending.getCategories('expense');
793
879
 
794
880
  // Settings
795
- const settings = await ctx.api.getSettings();
881
+ const settings = await ctx.api.settings.get();
796
882
 
797
883
  // Logging and debugging
798
884
  ctx.api.logger.info('Operation completed successfully');
799
- ctx.api.logger.error('Error occurred:', error);
800
- ctx.api.logger.debug('Debug info:', debugData);
885
+ ctx.api.logger.error(`Error occurred: ${String(error)}`);
886
+ ctx.api.logger.debug(`Debug info: ${JSON.stringify(debugData)}`);
801
887
  ```
802
888
 
803
889
  ### Available API Methods
804
890
 
805
- | Method | Description | Permission Required |
806
- | ----------------------------------------------- | --------------------------------------------------------- | -------------------- |
807
- | `portfolio.getHoldings(accountId)` | Get portfolio holdings for account | `portfolio` |
808
- | `portfolio.getHolding(accountId, assetId)` | Get specific holding | `portfolio` |
809
- | `portfolio.calculatePerformanceSummary(params)` | Calculate performance metrics | `portfolio` |
810
- | `portfolio.getIncomeSummary()` | Get income summary data | `portfolio` |
811
- | `accounts.getAll()` | Get all account information | `accounts` |
812
- | `accounts.create(account)` | Create new account | `accounts` |
813
- | `activities.getAll(accountId?)` | Get activity history (optionally filtered to one account) | `activities` |
814
- | `activities.create(activity)` | Create new activity | `activities` |
815
- | `marketData.getQuoteHistory(symbol)` | Get historical quotes | `market-data` |
816
- | `marketData.getAssetProfile(assetId)` | Get asset profile | `market-data` |
817
- | `marketData.searchTicker(query)` | Search for tickers | `market-data` |
818
- | `goals.getAll()` | Get financial goals | `financial-planning` |
819
- | `goals.getFunding(goalId)` | Get funding rules for a goal | `financial-planning` |
820
- | `goals.saveFunding(goalId, rules)` | Save funding rules for a goal | `financial-planning` |
821
- | `settings.get()` | Get app settings | `settings` |
822
- | `query.getClient()` | Get shared QueryClient instance | None |
891
+ | Method | Description | Permission Required |
892
+ | ------------------------------------------ | ------------------------------------------- | --------------------- |
893
+ | `portfolio.getHoldings(accountId)` | Get portfolio holdings for an account | `portfolio` |
894
+ | `portfolio.getHolding(accountId, assetId)` | Get a specific holding | `portfolio` |
895
+ | `performance.calculateSummary(params)` | Calculate performance metrics | `performance` |
896
+ | `accounts.getAll()` | Get all account information | `accounts` |
897
+ | `accounts.create(account)` | Create an account | `accounts` |
898
+ | `activities.getAll(accountId?)` | Get activity history | `activities` |
899
+ | `activities.create(activity)` | Create an activity | `activities` |
900
+ | `market.searchTicker(query)` | Search for tickers | `market-data` |
901
+ | `assets.getProfile(assetId)` | Get a financial asset profile | `assets` |
902
+ | `quotes.getHistory(assetId)` | Get historical quotes | `quotes` |
903
+ | `exchangeRates.getRatesForDates(pairs)` | Resolve dated exchange rates | `currency` |
904
+ | `spending.isEnabled()` | Check whether Spending is enabled | `spending` |
905
+ | `spending.getCategories(kind?)` | List expense, income, or savings categories | `spending` |
906
+ | `spending.getRules()` | List this addon's categorization rules | `spending` |
907
+ | `spending.saveRule(rule)` | Create or update an addon-owned rule | `spending` |
908
+ | `spending.deleteRule(ruleKey)` | Delete an addon-owned rule | `spending` |
909
+ | `spending.rerunRules(onlyUncategorized?)` | Re-run categorization rules | `spending` |
910
+ | `goals.getAll()` | Get financial goals | `financial-planning` |
911
+ | `contributionLimits.getAll()` | Get contribution limits | `contribution-limits` |
912
+ | `settings.get()` | Get application settings | `settings` |
913
+ | `query.getClient()` | Get this addon's QueryClient | None |
914
+
915
+ The Spending and dated exchange-rate APIs require Wealthfolio 3.8 or newer. See
916
+ the [complete API reference](../../docs/addons/addon-api-reference.md).
917
+
918
+ ### Localization
919
+
920
+ Wealthfolio 3.8 addons can register private translation bundles with
921
+ `registerTranslations()` and read them from React components with
922
+ `useAddonTranslation()`. Addons using these exports must set
923
+ `minWealthfolioVersion` to `3.8.0` or newer. See the
924
+ [localization guide](../../docs/addons/addon-localization.md).
823
925
 
824
926
  > Tip: `activities.getAll` accepts an optional account ID string to scope
825
927
  > results to a single account. The SDK normalizes this for both desktop (Tauri)
@@ -856,51 +958,51 @@ const response = await ctx.api.activities.search(
856
958
  The SDK provides a comprehensive logging system:
857
959
 
858
960
  ```typescript
859
- const ctx = getAddonContext();
961
+ // Use the ctx parameter supplied to enable(ctx), or pass it to this helper.
860
962
 
861
- // Log levels: 'error', 'warn', 'info', 'debug'
862
- ctx.api.logger.error('Critical error occurred', { error, context });
863
- ctx.api.logger.warn('Warning message', additionalData);
963
+ // Each method accepts one string message.
964
+ ctx.api.logger.error(`Critical error occurred: ${String(error)}`);
965
+ ctx.api.logger.warn(`Warning message: ${String(additionalData)}`);
864
966
  ctx.api.logger.info('Information message');
865
- ctx.api.logger.debug('Debug information', debugObject);
866
-
867
- // Set log level (for development)
868
- ctx.api.logger.setLevel('debug');
869
-
870
- // Check if logging level is enabled
871
- if (ctx.api.logger.isLevelEnabled('debug')) {
872
- ctx.api.logger.debug('Expensive debug operation', expensiveData);
873
- }
967
+ ctx.api.logger.debug(`Debug information: ${JSON.stringify(debugObject)}`);
968
+ ctx.api.logger.trace('Detailed trace message');
874
969
  ```
875
970
 
876
- ### Shared QueryClient Integration
971
+ ### Addon QueryClient Integration
877
972
 
878
- The SDK provides access to Wealthfolio's shared React Query client for
879
- consistent data fetching and caching:
973
+ The sandbox provides one React Query client per addon. Its cache is reused
974
+ across that addon's route renders, not shared with the host or other addons.
975
+ Invalidate/refetch operations are mirrored to the host:
880
976
 
881
977
  ```typescript
882
- // Access the shared QueryClient instance
883
- const sharedQueryClient = context.api.query.getClient();
978
+ import { QueryClientProvider, useQuery } from '@tanstack/react-query';
979
+ import type { AddonContext, QueryClient } from '@wealthfolio/addon-sdk';
884
980
 
885
981
  // Wrap your components with QueryClientProvider
886
- const MyAddonWrapper = () => {
982
+ const MyAddonWrapper = ({ ctx }: { ctx: AddonContext }) => {
983
+ const addonQueryClient = ctx.api.query.getClient() as QueryClient;
984
+
887
985
  return (
888
- <QueryClientProvider client={sharedQueryClient}>
889
- <MyAddonComponent />
986
+ <QueryClientProvider client={addonQueryClient}>
987
+ <MyAddonComponent ctx={ctx} />
890
988
  </QueryClientProvider>
891
989
  );
892
990
  };
893
991
 
894
992
  // Use React Query hooks in your components
895
- function MyAddonComponent() {
993
+ function MyAddonComponent({ ctx }: { ctx: AddonContext }) {
896
994
  const { data: accounts, isLoading } = useQuery({
897
995
  queryKey: ['accounts'],
898
996
  queryFn: () => ctx.api.accounts.getAll()
899
997
  });
900
998
 
999
+ const selectedAccountId = accounts?.[0]?.id;
901
1000
  const { data: holdings } = useQuery({
902
1001
  queryKey: ['holdings', selectedAccountId],
903
- queryFn: () => ctx.api.portfolio.getHoldings(selectedAccountId),
1002
+ queryFn: () =>
1003
+ selectedAccountId
1004
+ ? ctx.api.portfolio.getHoldings(selectedAccountId)
1005
+ : Promise.resolve([]),
904
1006
  enabled: !!selectedAccountId
905
1007
  });
906
1008
 
@@ -908,15 +1010,24 @@ function MyAddonComponent() {
908
1010
  }
909
1011
  ```
910
1012
 
911
- **Benefits of Shared QueryClient:**
1013
+ **Benefits of the sandbox-scoped QueryClient:**
912
1014
 
913
- - **Consistent Caching**: Share cache with the main application
914
- - **Performance**: Avoid duplicate API calls across addons
915
- - **Synchronization**: Real-time updates when data changes
916
- - **Memory Efficiency**: Single cache instance for all data
1015
+ - **Isolation**: Cached financial data and observers do not leak across addons
1016
+ - **Route continuity**: One cache is retained across the addon's pages
1017
+ - **Coordination**: Addon invalidations/refetches are also sent to the host
1018
+ - **Lifecycle cleanup**: The cache is cleared with the addon sandbox
1019
+
1020
+ Host-originated invalidations do not mutate the addon cache automatically. Use
1021
+ the relevant `ctx.api.events` subscription and invalidate locally when the addon
1022
+ must react to changes initiated elsewhere.
917
1023
 
918
1024
  ## 🔄 Migration Guide
919
1025
 
1026
+ For Wealthfolio 3.7, see the
1027
+ [v3.6 to v3.7 migration guide](../../docs/addons/addon-migration-guide-v3.6-to-v3.7.md).
1028
+ It covers backward compatibility, the private asset registry, CSS behavior, and
1029
+ the required development-tools upgrade.
1030
+
920
1031
  ### From v1.0.0 to v1.1.0
921
1032
 
922
1033
  #### Context Access
@@ -925,9 +1036,14 @@ function MyAddonComponent() {
925
1036
  // Before
926
1037
  import ctx from '@wealthfolio/addon-sdk';
927
1038
 
928
- // After (recommended)
929
- import { getAddonContext } from '@wealthfolio/addon-sdk';
930
- const ctx = getAddonContext();
1039
+ // Current SDK
1040
+ import type { AddonEnableFunction } from '@wealthfolio/addon-sdk';
1041
+
1042
+ const enable: AddonEnableFunction = (ctx) => {
1043
+ // Pass ctx to components, hooks, and helper functions that need host APIs.
1044
+ };
1045
+
1046
+ export default enable;
931
1047
  ```
932
1048
 
933
1049
  #### Type Imports
@@ -1008,6 +1124,7 @@ import { resolve } from 'path';
1008
1124
  export default defineConfig({
1009
1125
  plugins: [react()],
1010
1126
  build: {
1127
+ target: ['chrome107', 'edge107', 'firefox104', 'safari16'],
1011
1128
  lib: {
1012
1129
  entry: resolve(__dirname, 'src/index.ts'),
1013
1130
  name: 'MyPortfolioAddon',
@@ -1153,9 +1270,9 @@ npm publish --tag beta
1153
1270
 
1154
1271
  ```typescript
1155
1272
  // In your addon
1156
- const ctx = getAddonContext();
1157
- ctx.api.logger.setLevel('debug');
1158
- ctx.api.logger.debug('Debug information:', data);
1273
+ function logDebug(ctx: AddonContext, data: unknown) {
1274
+ ctx.api.logger.debug(`Debug information: ${JSON.stringify(data)}`);
1275
+ }
1159
1276
  ```
1160
1277
 
1161
1278
  #### 2. Development Console
@@ -1186,11 +1303,9 @@ if (process.env.NODE_ENV === 'development') {
1186
1303
  #### 1. Error Handling
1187
1304
 
1188
1305
  ```typescript
1189
- import { getAddonContext } from '@wealthfolio/addon-sdk';
1190
-
1191
- async function fetchPortfolioData() {
1192
- const ctx = getAddonContext();
1306
+ import type { AddonContext } from '@wealthfolio/addon-sdk';
1193
1307
 
1308
+ async function fetchPortfolioData(ctx: AddonContext) {
1194
1309
  try {
1195
1310
  // Get all accounts first, then holdings for each
1196
1311
  const accounts = await ctx.api.accounts.getAll();
@@ -1199,14 +1314,8 @@ async function fetchPortfolioData() {
1199
1314
  ).then((results) => results.flat());
1200
1315
  return holdings;
1201
1316
  } catch (error) {
1202
- ctx.api.logger.error('Failed to fetch holdings:', error);
1203
-
1204
- // Handle different error types
1205
- if (error.code === 'PERMISSION_DENIED') {
1206
- // Show permission error to user
1207
- } else if (error.code === 'NETWORK_ERROR') {
1208
- // Handle network issues
1209
- }
1317
+ const message = error instanceof Error ? error.message : String(error);
1318
+ ctx.api.logger.error(`Failed to fetch holdings: ${message}`);
1210
1319
 
1211
1320
  throw error;
1212
1321
  }
@@ -1270,17 +1379,15 @@ const HeavyChart = lazy(() => import('./components/HeavyChart'));
1270
1379
 
1271
1380
  ```typescript
1272
1381
  // Use React Query or SWR for caching
1273
- import { useQuery } from 'react-query';
1382
+ import { useQuery } from '@tanstack/react-query';
1274
1383
 
1275
1384
  function usePortfolioData(accountId: string) {
1276
- return useQuery(
1277
- ['portfolio', accountId],
1278
- () => ctx.api.portfolio.getHoldings(accountId),
1279
- {
1280
- staleTime: 5 * 60 * 1000, // 5 minutes
1281
- cacheTime: 10 * 60 * 1000, // 10 minutes
1282
- },
1283
- );
1385
+ return useQuery({
1386
+ queryKey: ['portfolio', accountId],
1387
+ queryFn: () => ctx.api.portfolio.getHoldings(accountId),
1388
+ staleTime: 5 * 60 * 1000, // 5 minutes
1389
+ gcTime: 10 * 60 * 1000, // 10 minutes
1390
+ });
1284
1391
  }
1285
1392
  ```
1286
1393
 
@@ -1290,6 +1397,7 @@ function usePortfolioData(accountId: string) {
1290
1397
  // vite.config.ts - optimize chunks
1291
1398
  export default defineConfig({
1292
1399
  build: {
1400
+ target: ['chrome107', 'edge107', 'firefox104', 'safari16'],
1293
1401
  rollupOptions: {
1294
1402
  output: {
1295
1403
  manualChunks: {
@@ -1382,7 +1490,8 @@ We follow [Semantic Versioning](https://semver.org/) (SemVer):
1382
1490
 
1383
1491
  | SDK Version | Wealthfolio Version | Node.js | React |
1384
1492
  | ----------- | ------------------- | --------- | ------- |
1385
- | 1.0.x | >= 1.0.0 | >= 18.0.0 | ^18.0.0 |
1493
+ | 3.8.x | >= 3.8.0 | >= 20.0.0 | ^19.2.4 |
1494
+ | 3.7.x | >= 3.7.0 | >= 20.0.0 | ^19.2.4 |
1386
1495
  | 0.9.x | >= 0.9.0 | >= 16.0.0 | ^17.0.0 |
1387
1496
 
1388
1497
  ### Installation from Registry
@@ -1394,10 +1503,10 @@ We follow [Semantic Versioning](https://semver.org/) (SemVer):
1394
1503
  npm install @wealthfolio/addon-sdk
1395
1504
 
1396
1505
  # Specific version
1397
- npm install @wealthfolio/addon-sdk@1.0.0
1506
+ npm install @wealthfolio/addon-sdk@3.8.0
1398
1507
 
1399
1508
  # Version range
1400
- npm install @wealthfolio/addon-sdk@^1.0.0
1509
+ npm install @wealthfolio/addon-sdk@^3.8.0
1401
1510
  ```
1402
1511
 
1403
1512
  #### Beta/Preview Releases
@@ -1633,6 +1742,7 @@ npm list react react-dom
1633
1742
  // vite.config.ts
1634
1743
  export default defineConfig({
1635
1744
  build: {
1745
+ target: ['chrome107', 'edge107', 'firefox104', 'safari16'],
1636
1746
  rollupOptions: {
1637
1747
  external: ['react', 'react-dom', '@wealthfolio/addon-sdk'],
1638
1748
  },
@@ -1652,31 +1762,29 @@ export default defineConfig({
1652
1762
  "permissions": [
1653
1763
  {
1654
1764
  "category": "portfolio",
1655
- "functions": ["holdings"],
1765
+ "functions": ["getHoldings"],
1656
1766
  "purpose": "Access portfolio data for analytics"
1657
1767
  }
1658
1768
  ]
1659
1769
  }
1660
1770
  ```
1661
1771
 
1662
- #### 6. Context Not Available
1772
+ #### 6. Context Not Available in a Component or Helper
1663
1773
 
1664
- **Error**: `getAddonContext() returns undefined`
1774
+ **Error**: A component or helper cannot access the addon context.
1665
1775
 
1666
1776
  **Solutions**:
1667
1777
 
1668
1778
  ```typescript
1669
- // Ensure you're calling it within addon context
1670
- function MyComponent() {
1671
- useEffect(() => {
1672
- // Call context inside useEffect or event handlers
1673
- const ctx = getAddonContext();
1674
- // ... use context
1675
- }, []);
1779
+ import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk';
1780
+
1781
+ function MyComponent({ ctx }: { ctx: AddonContext }) {
1782
+ return <button onClick={() => ctx.api.toast.success('Ready')}>Test API</button>;
1676
1783
  }
1677
1784
 
1678
- // Don't call at module level
1679
- // const ctx = getAddonContext(); // Wrong
1785
+ const enable: AddonEnableFunction = (ctx) => {
1786
+ // Capture ctx for a route wrapper, or pass it directly to helpers/components.
1787
+ };
1680
1788
  ```
1681
1789
 
1682
1790
  ### Development Environment Issues
@@ -1722,13 +1830,10 @@ ls -la dist/ # Should update when you save files
1722
1830
  try {
1723
1831
  const accounts = await ctx.api.accounts.getAll();
1724
1832
  const data = await ctx.api.portfolio.getHoldings(accounts[0]?.id);
1725
- ctx.api.logger.info('Data loaded successfully', { count: data.length });
1833
+ ctx.api.logger.info(`Data loaded successfully (${data.length} holdings)`);
1726
1834
  } catch (error) {
1727
- ctx.api.logger.error('API call failed', {
1728
- error: error.message,
1729
- stack: error.stack,
1730
- timestamp: new Date().toISOString(),
1731
- });
1835
+ const message = error instanceof Error ? error.message : String(error);
1836
+ ctx.api.logger.error(`API call failed: ${message}`);
1732
1837
  }
1733
1838
  ```
1734
1839
 
@@ -1744,6 +1849,7 @@ const HeavyComponent = lazy(() => import('./HeavyComponent'));
1744
1849
  // vite.config.ts
1745
1850
  export default defineConfig({
1746
1851
  build: {
1852
+ target: ['chrome107', 'edge107', 'firefox104', 'safari16'],
1747
1853
  rollupOptions: {
1748
1854
  output: {
1749
1855
  manualChunks: {