Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

## Agent Working Guidelines (short)
- Keep public API compatibility in `src/index.package.ts` and `src/index.bundle.ts`.
- React Compiler is enabled. Do not add `useMemo`, `useCallback`, or `React.memo` only for render-performance optimization; prefer plain values/functions. Use manual memoization only when required for semantic stability, such as an external API dependency or context value identity.
- React Compiler is enabled. Do not add `useMemo`, `useCallback`, or `React.memo` only for render-performance optimization; prefer plain values/functions.
- When changing user-facing copy, update both:
- `packages/widget/src/translation/English/translations.json`
- `packages/widget/src/translation/French/translations.json`
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,39 @@ type SettingsProps = {
| Record<SupportedSKChains, string>
| ((chain: SupportedSKChains) => string);
dashboardVariant?: boolean;
dashboardYieldCategoryOrder?: DashboardYieldCategory[];
hideChainSelector?: boolean;
hideAccountAndChainSelector?: boolean;
preferredTokenYieldsPerNetwork?: PreferredTokenYieldsPerNetwork;
portalContainer?: HTMLElement;
};
```

### Dashboard category tab order

For the dashboard variant, you can reorder the `RWA`, `DeFi`, and `Stake`
category tabs. `Manage` and `Activity` stay fixed after those category tabs.

```tsx
import "@stakekit/widget/package/css";
import { DashboardYieldCategory, SKApp, darkTheme } from "@stakekit/widget";

const App = () => {
return (
<SKApp
apiKey="your-api-key"
dashboardVariant
dashboardYieldCategoryOrder={[
DashboardYieldCategory.Stake,
DashboardYieldCategory.DeFi,
DashboardYieldCategory.RWA,
]}
theme={darkTheme}
/>
);
};
```

### Override Icons

You can override token or chain icons in widget
Expand Down
41 changes: 37 additions & 4 deletions packages/widget/src/domain/types/yields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,47 @@ export type ValidatorsConfig = Map<
}
>;

export type DashboardYieldCategory = "stake" | "defi" | "rwa";
export const DashboardYieldCategory = {
RWA: "rwa",
DeFi: "defi",
Stake: "stake",
} as const;

export type DashboardYieldCategory =
(typeof DashboardYieldCategory)[keyof typeof DashboardYieldCategory];

export const dashboardYieldCategories = [
"rwa",
"defi",
"stake",
DashboardYieldCategory.RWA,
DashboardYieldCategory.DeFi,
DashboardYieldCategory.Stake,
] as const satisfies ReadonlyArray<DashboardYieldCategory>;

const dashboardYieldCategoryValues = new Set<string>(dashboardYieldCategories);

export const normalizeDashboardYieldCategoryOrder = (
order?: ReadonlyArray<unknown> | null
): DashboardYieldCategory[] => {
const normalized: DashboardYieldCategory[] = [];

for (const category of order ?? []) {
if (
typeof category === "string" &&
dashboardYieldCategoryValues.has(category) &&
!normalized.includes(category as DashboardYieldCategory)
) {
normalized.push(category as DashboardYieldCategory);
}
}

for (const category of dashboardYieldCategories) {
if (!normalized.includes(category)) {
normalized.push(category);
}
}

return normalized;
};

/**
* Maps every API `YieldType` to exactly one dashboard category. The
* `satisfies Record<ApiYieldType, ...>` guarantees exhaustiveness: a new server
Expand Down
9 changes: 5 additions & 4 deletions packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { useMemo } from "react";
import type { TokenDto } from "../../domain/types/tokens";
import {
type DashboardYieldCategory,
dashboardYieldCategories,
getApiYieldTypesForDashboardCategory,
} from "../../domain/types/yields";
import { useApiClient } from "../../providers/api/api-client-provider";
import { useSettings } from "../../providers/settings";
import { useSKWallet } from "../../providers/sk-wallet";
import {
DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT,
Expand Down Expand Up @@ -38,11 +38,12 @@ export const useDashboardYieldCatalog = ({
} = {}) => {
const { network, isConnecting } = useSKWallet();
const apiClient = useApiClient();
const { dashboardYieldCategoryOrder } = useSettings();

const probeEnabled = enabled && !isConnecting;

const results = useQueries({
queries: dashboardYieldCategories.map((category) => {
queries: dashboardYieldCategoryOrder.map((category) => {
const params: YieldSummariesParams = {
...(network ? { network: network } : {}),
types: getApiYieldTypesForDashboardCategory(category),
Expand All @@ -67,7 +68,7 @@ export const useDashboardYieldCatalog = ({
>();
const availableCategories: DashboardYieldCategory[] = [];

dashboardYieldCategories.forEach((category, index) => {
dashboardYieldCategoryOrder.forEach((category, index) => {
const firstVisible = (results[index]?.data ?? []).find(
isVisibleYieldSummary
);
Expand All @@ -87,5 +88,5 @@ export const useDashboardYieldCatalog = ({
initialSelectionByCategory,
isLoading: probeEnabled && results.some((result) => result.isLoading),
};
}, [results, probeEnabled]);
}, [dashboardYieldCategoryOrder, results, probeEnabled]);
};
1 change: 1 addition & 0 deletions packages/widget/src/index.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export { EvmChainIds } from "./domain/types/chains/evm";
export { MiscChainIds } from "./domain/types/chains/misc";
export { SubstrateChainIds } from "./domain/types/chains/substrate";
export type * from "./domain/types/wallets/generic-wallet";
export { DashboardYieldCategory } from "./domain/types/yields";
export { darkTheme, lightTheme } from "./styles/theme/themes";
1 change: 1 addition & 0 deletions packages/widget/src/index.package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { EvmChainIds } from "./domain/types/chains/evm";
export { MiscChainIds } from "./domain/types/chains/misc";
export { SubstrateChainIds } from "./domain/types/chains/substrate";
export type * from "./domain/types/wallets/generic-wallet";
export { DashboardYieldCategory } from "./domain/types/yields";
export { TrackingContextProvider } from "./providers/tracking";
export { createWallet } from "./providers/wagmi/utils";
export { darkTheme, lightTheme } from "./styles/theme/themes";
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useQueries } from "@tanstack/react-query";
import {
type DashboardYieldCategory,
dashboardYieldCategories,
getDashboardYieldCategory,
} from "../../../../domain/types/yields";
import { queryFn } from "../../../../hooks/api/use-yield-opportunity/get-yield-opportunity";
Expand Down Expand Up @@ -33,7 +32,7 @@ export const useGroupedPositions = (
const { isLedgerLive } = useSKWallet();
const apiClient = useApiClient();
const queryClient = useSKQueryClient();
const { yieldGrouping } = useSettings();
const { dashboardYieldCategoryOrder, yieldGrouping } = useSettings();
const dashboardYieldCategoryGroupingEnabled = yieldGrouping === "category";

const integrationIds = dashboardYieldCategoryGroupingEnabled
Expand Down Expand Up @@ -83,7 +82,7 @@ export const useGroupedPositions = (

const rows: PositionsListRow[] = [{ kind: "chain-modal" }];

for (const category of dashboardYieldCategories) {
for (const category of dashboardYieldCategoryOrder) {
const items = grouped.get(category);
if (!items?.length) continue;

Expand Down
6 changes: 6 additions & 0 deletions packages/widget/src/providers/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { PropsWithChildren } from "react";
import { createContext, useContext, useLayoutEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { config } from "../../config";
import { normalizeDashboardYieldCategoryOrder } from "../../domain/types/yields";
import utilaTranslations from "../../translation/English/utila-variant.json";
import type { SettingsContextType, SettingsProps, VariantProps } from "./types";

Expand Down Expand Up @@ -39,6 +40,10 @@ export const SettingsContextProvider = ({
.extract() as typeof rest.preferredTokenYieldsPerNetwork;
}, [rest.preferredTokenYieldsPerNetwork]);

const dashboardYieldCategoryOrder = normalizeDashboardYieldCategoryOrder(
rest.dashboardYieldCategoryOrder
);

const { i18n } = useTranslation();

useLayoutEffect(() => {
Expand Down Expand Up @@ -69,6 +74,7 @@ export const SettingsContextProvider = ({
<SettingsContext.Provider
value={{
...rest,
dashboardYieldCategoryOrder,
preferredTokenYieldsPerNetwork,
yieldGrouping:
rest.yieldGrouping ?? (rest.dashboardVariant ? "category" : "flat"),
Expand Down
8 changes: 7 additions & 1 deletion packages/widget/src/providers/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { TransactionFormat } from "../../domain/types/settings";
import type { PreferredTokenYieldsPerNetwork } from "../../domain/types/stake";
import type { TokenDto } from "../../domain/types/tokens";
import type { SKExternalProviders } from "../../domain/types/wallets";
import type { DashboardYieldCategory } from "../../domain/types/yields";
import type { Languages, localResources } from "../../translation/resources";
import type { RecursivePartial } from "../../types/utils";
import type { ThemeWrapperTheme } from "../theme-wrapper-types";
Expand Down Expand Up @@ -86,6 +87,7 @@ export type SettingsProps = {
| Record<SupportedSKChains, string>
| ((chain: SupportedSKChains) => string);
dashboardVariant?: boolean;
dashboardYieldCategoryOrder?: DashboardYieldCategory[];
yieldGrouping?: YieldGrouping;
institutionalWallets?: boolean;
hideChainSelector?: boolean;
Expand All @@ -96,7 +98,11 @@ export type SettingsProps = {
initialChain?: SupportedSKChainIds;
};

type ResolvedSettingsProps = Omit<SettingsProps, "yieldGrouping"> & {
type ResolvedSettingsProps = Omit<
SettingsProps,
"dashboardYieldCategoryOrder" | "yieldGrouping"
> & {
dashboardYieldCategoryOrder: DashboardYieldCategory[];
yieldGrouping: YieldGrouping;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, it } from "vitest";
import {
DashboardYieldCategory,
dashboardYieldCategories,
getApiYieldTypesForDashboardCategory,
getYieldTypesSortRank,
normalizeDashboardYieldCategoryOrder,
type YieldBase,
} from "../../src/domain/types/yields";

Expand Down Expand Up @@ -53,6 +55,51 @@ describe("getApiYieldTypesForDashboardCategory", () => {
});
});

describe("normalizeDashboardYieldCategoryOrder", () => {
it("uses the default category order when no order is provided", () => {
expect(normalizeDashboardYieldCategoryOrder()).toEqual([
"rwa",
"defi",
"stake",
]);
});

it("preserves a complete custom category order", () => {
expect(
normalizeDashboardYieldCategoryOrder([
DashboardYieldCategory.Stake,
DashboardYieldCategory.DeFi,
DashboardYieldCategory.RWA,
])
).toEqual([
DashboardYieldCategory.Stake,
DashboardYieldCategory.DeFi,
DashboardYieldCategory.RWA,
]);
});

it("deduplicates categories by keeping their first occurrence", () => {
expect(
normalizeDashboardYieldCategoryOrder([
DashboardYieldCategory.Stake,
DashboardYieldCategory.DeFi,
DashboardYieldCategory.Stake,
DashboardYieldCategory.RWA,
])
).toEqual([
DashboardYieldCategory.Stake,
DashboardYieldCategory.DeFi,
DashboardYieldCategory.RWA,
]);
});

it("ignores runtime-invalid entries and appends missing valid categories", () => {
expect(
normalizeDashboardYieldCategoryOrder(["stake", "invalid", "defi"])
).toEqual(["stake", "defi", "rwa"]);
});
});

describe("getYieldTypesSortRank", () => {
const makeYield = (type: YieldBase["mechanics"]["type"]): YieldBase =>
({
Expand Down
33 changes: 33 additions & 0 deletions packages/widget/tests/use-cases/renders-initial-page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { delay, HttpResponse, http } from "msw";
import { DashboardYieldCategory } from "../../src/domain/types/yields";
import { legacyYieldFixture, yieldApiYieldFixture } from "../fixtures";
import { legacyApiRoute, yieldApiRoute } from "../mocks/api-routes";
import { describe, expect, it } from "../utils/test-extend";
Expand Down Expand Up @@ -179,4 +180,36 @@ describe("Renders initial page", () => {

app.unmount();
});

it("uses the configured dashboard category tab order", async () => {
const app = await renderApp({
skProps: {
apiKey: import.meta.env.VITE_API_KEY,
dashboardVariant: true,
dashboardYieldCategoryOrder: [
DashboardYieldCategory.Stake,
DashboardYieldCategory.DeFi,
DashboardYieldCategory.RWA,
],
},
});

await expect.element(app.getByText("Stake")).toBeInTheDocument();
await expect.element(app.getByText("DeFi")).toBeInTheDocument();
await expect.element(app.getByText("RWA")).toBeInTheDocument();
await expect.element(app.getByText("Manage")).toBeInTheDocument();
await expect.element(app.getByText("Activity")).toBeInTheDocument();

const tabsSection = app.container.querySelector("[data-rk='tabs-section']");
const tabsText = tabsSection?.textContent ?? "";

expect(tabsText.indexOf("Stake")).toBeLessThan(tabsText.indexOf("DeFi"));
expect(tabsText.indexOf("DeFi")).toBeLessThan(tabsText.indexOf("RWA"));
expect(tabsText.indexOf("RWA")).toBeLessThan(tabsText.indexOf("Manage"));
expect(tabsText.indexOf("Manage")).toBeLessThan(
tabsText.indexOf("Activity")
);

app.unmount();
});
});