diff --git a/.env.example b/.env.example index 64a1486..14826d9 100644 --- a/.env.example +++ b/.env.example @@ -120,3 +120,6 @@ NEXT_PUBLIC_AI_CREDITS_WIDGET_DASHBOARD_ENABLED=false # Hide the dashboard button while keeping /{locale}/superfluid-campaign accessible. NEXT_PUBLIC_SUPERFLUID_CAMPAIGN_WIDGET_DASHBOARD_ENABLED=false + +# Hide the dashboard button while keeping /{locale}/streaming accessible. +NEXT_PUBLIC_STREAMING_WIDGET_DASHBOARD_ENABLED=false diff --git a/README.md b/README.md index 24466c4..ee2d3fb 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,10 @@ The Superfluid button follows the same pattern with `NEXT_PUBLIC_SUPERFLUID_CAMPAIGN_WIDGET_DASHBOARD_ENABLED`; its authenticated `/{locale}/superfluid-campaign` path remains available when the button is hidden. +The Streaming button follows the same pattern with +`NEXT_PUBLIC_STREAMING_WIDGET_DASHBOARD_ENABLED`; its authenticated +`/{locale}/streaming` path remains available when the button is hidden. + ## GoodWidget local-tarball workflow Widget packages that are not yet published to npm (e.g. `@goodwidget/ai-credits-widget`) diff --git a/next.config.mjs b/next.config.mjs index 5c3976a..9797199 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,6 +10,7 @@ const nextConfig = { "@goodwidget/citizen-claim-widget", "@goodwidget/ai-credits-widget", "@goodwidget/goodreserve-widget", + "@goodwidget/streaming-widget", "@goodwidget/core", "@goodwidget/embed", "@goodwidget/ui", diff --git a/package.json b/package.json index 11858be..c6f85b4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@goodwidget/core": "0.1.5", "@goodwidget/embed": "0.1.4", "@goodwidget/goodreserve-widget": "0.1.7", + "@goodwidget/streaming-widget": "0.1.1", "@goodwidget/superfluid-campaign-widget": "0.1.7", "@goodwidget/ui": "0.1.6", "@hcaptcha/react-hcaptcha": "^2.0.2", diff --git a/src/analytics/types.ts b/src/analytics/types.ts index d69c5a9..a1cca8b 100644 --- a/src/analytics/types.ts +++ b/src/analytics/types.ts @@ -51,6 +51,7 @@ export enum AnalyticsEventTypes { AICreditsTabSelected = "AICredits_Tab_Selected", GoodReserveTabSelected = "GoodReserve_Tab_Selected", SuperfluidCampaignTabSelected = "SuperfluidCampaign_Tab_Selected", + StreamingTabSelected = "Streaming_Tab_Selected", } export enum AvailableMethodsNames { @@ -273,3 +274,6 @@ export type AnalyticsEvent = | { type: AnalyticsEventTypes.SuperfluidCampaignTabSelected } + | { + type: AnalyticsEventTypes.StreamingTabSelected + } diff --git a/src/sections/Home/components/WalletSection.tsx b/src/sections/Home/components/WalletSection.tsx index a5834b9..2a00b59 100644 --- a/src/sections/Home/components/WalletSection.tsx +++ b/src/sections/Home/components/WalletSection.tsx @@ -45,12 +45,14 @@ type WidgetTabSelectedEventType = | AnalyticsEventTypes.AICreditsTabSelected | AnalyticsEventTypes.GoodReserveTabSelected | AnalyticsEventTypes.SuperfluidCampaignTabSelected + | AnalyticsEventTypes.StreamingTabSelected const widgetActionEventType: Record = { "goodwidget.ai-credits": AnalyticsEventTypes.AICreditsTabSelected, "goodwidget.goodreserve": AnalyticsEventTypes.GoodReserveTabSelected, "goodwidget.superfluid-campaign": AnalyticsEventTypes.SuperfluidCampaignTabSelected, + "goodwidget.streaming": AnalyticsEventTypes.StreamingTabSelected, } export default function WalletSection({ diff --git a/src/ui/assets/index.ts b/src/ui/assets/index.ts index 6a39cbe..4b18a2c 100644 --- a/src/ui/assets/index.ts +++ b/src/ui/assets/index.ts @@ -41,7 +41,7 @@ import { import { FaCaretDown, FaCaretUp } from "react-icons/fa" import { HiDotsHorizontal } from "react-icons/hi" import { IoDocumentOutline } from "react-icons/io5" -import { LuSparkles } from "react-icons/lu" +import { LuSparkles, LuWaves } from "react-icons/lu" import { MdOutlineContactSupport, MdOutlinePrivacyTip } from "react-icons/md" import { RiArrowDownLine, RiArrowUpLine } from "react-icons/ri" import { TbCube3dSphere, TbLogout } from "react-icons/tb" @@ -121,6 +121,7 @@ export const UIIcons = { Cash: BsCashStack, Legal: IoDocumentOutline, Sparkles: LuSparkles, + Waves: LuWaves, Logout: TbLogout, Privacy: MdOutlinePrivacyTip, Questionmark: BsFillQuestionCircleFill, diff --git a/src/widgets/fixtures/streamingWidgetRegisterMock.ts b/src/widgets/fixtures/streamingWidgetRegisterMock.ts new file mode 100644 index 0000000..0a1f284 --- /dev/null +++ b/src/widgets/fixtures/streamingWidgetRegisterMock.ts @@ -0,0 +1,14 @@ +/** + * Vitest mock for `@goodwidget/streaming-widget/register`. + * + * The real module performs a Custom Element side-effect registration which + * requires a browser DOM. This no-op stub satisfies the import during unit + * tests without triggering DOM APIs. + */ +export const goodWidgetMetadata = { + packageName: "@goodwidget/streaming-widget", + packageVersion: "0.1.1", +} + +export const register = async (tagName?: string): Promise => + tagName ?? "gw-streaming" diff --git a/src/widgets/registry.test.ts b/src/widgets/registry.test.ts index bf16fb6..2033b7c 100644 --- a/src/widgets/registry.test.ts +++ b/src/widgets/registry.test.ts @@ -273,4 +273,57 @@ describe("widget registry", () => { ]) }) }) + + describe("Streaming widget", () => { + it("is present in the live WIDGETS array with the correct widgetId", () => { + const streaming = WIDGETS.find( + (w) => w.widgetId === "goodwidget.streaming", + ) + expect(streaming).toBeDefined() + expect(streaming).toMatchObject({ + widgetId: "goodwidget.streaming", + packageName: "@goodwidget/streaming-widget", + packageVersion: "0.1.1", + routeSlug: "streaming", + entry: { tagName: "gw-streaming" }, + providerPolicy: { + chainIds: [42220, 8453], + }, + }) + }) + + it("passes registry validation (createWidgetRegistry does not throw)", () => { + const streaming = WIDGETS.find( + (w) => w.widgetId === "goodwidget.streaming", + ) + expect(streaming).toBeDefined() + if (!streaming) return + expect(() => createWidgetRegistry([streaming])).not.toThrow() + }) + + it("appears in widgetDashboardActions only when dashboardVisible", () => { + const streaming = WIDGETS.find( + (w) => w.widgetId === "goodwidget.streaming", + ) + const action = widgetDashboardActions.find( + (a) => a.widgetId === "goodwidget.streaming", + ) + if (streaming?.dashboardVisible === false) { + expect(action).toBeUndefined() + } else { + expect(action).toBeDefined() + expect(action?.routeSlug).toBe("streaming") + expect(action?.label).toBe("Streaming") + } + }) + + it("uses the shared provider method allowlist", () => { + const streaming = WIDGETS.find( + (w) => w.widgetId === "goodwidget.streaming", + ) + expect(streaming?.providerPolicy.requiredMethods).toEqual([ + ...WIDGET_PROVIDER_METHOD_LIST, + ]) + }) + }) }) diff --git a/src/widgets/registry.ts b/src/widgets/registry.ts index 42e0d47..c7448ae 100644 --- a/src/widgets/registry.ts +++ b/src/widgets/registry.ts @@ -267,10 +267,43 @@ const goodReserveWidget = defineWidget({ }, }) +/** + * Superfluid streaming widget: money streams, GDA pool memberships, and Super + * Token balances. The package registers its Custom Element as a side effect + * when its register entry is imported. + */ +const streamingWidget = defineWidget({ + widgetId: "goodwidget.streaming", + packageName: "@goodwidget/streaming-widget", + packageVersion: "0.1.1", + routeSlug: "streaming", + displayName: "Streaming", + description: "Manage money streams, pools, and Super Token balances", + dashboardVisible: readBooleanEnv( + process.env.NEXT_PUBLIC_STREAMING_WIDGET_DASHBOARD_ENABLED, + false, + ), + icon: { kind: "system", name: "Waves" }, + integrationMode: "web-component", + entry: { + tagName: "gw-streaming", + load: () => import("@goodwidget/streaming-widget/register"), + }, + providerPolicy: { + chainIds: [CELO_CHAIN_ID, BASE_CHAIN_ID], + requiredMethods: WIDGET_PROVIDER_METHOD_LIST, + }, +}) + export const WIDGETS: readonly RegisteredWidget[] = process.env.NEXT_PUBLIC_PLAYWRIGHT_TEST_MODE === "true" ? [testFixtureWidget] - : [superfluidCampaignWidget, aiCreditsWidget, goodReserveWidget] + : [ + superfluidCampaignWidget, + aiCreditsWidget, + goodReserveWidget, + streamingWidget, + ] export const widgetRegistry = createWidgetRegistry(WIDGETS) export const getWidgetByRoute = ( diff --git a/vitest.config.ts b/vitest.config.ts index c43d5fd..a0d498d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -29,6 +29,11 @@ export default defineConfig({ __dirname, "./src/widgets/fixtures/aiCreditsWidgetRegisterMock.ts", ), + // Same for the Streaming widget register module. + "@goodwidget/streaming-widget/register": path.resolve( + __dirname, + "./src/widgets/fixtures/streamingWidgetRegisterMock.ts", + ), }, }, }) diff --git a/yarn.lock b/yarn.lock index 397ee10..671a4f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -928,6 +928,19 @@ __metadata: languageName: node linkType: hard +"@goodsdks/streaming-sdk@npm:1.1.0": + version: 1.1.0 + resolution: "@goodsdks/streaming-sdk@npm:1.1.0" + dependencies: + "@sfpro/sdk": "npm:^0.1.0" + graphql: "npm:^16.9.0" + graphql-request: "npm:^7.1.2" + peerDependencies: + viem: "*" + checksum: 10c0/ed7236e787a47474308d0950342c67c9cfdc9d68bc7aa01464e7bd0bbc08ee97c6f880a2e7e9355dd09d2824885eef0a7e3868f5b7607c7aa6773b0fc8658538 + languageName: node + linkType: hard + "@goodwidget/ai-credits-widget@npm:0.1.12": version: 0.1.12 resolution: "@goodwidget/ai-credits-widget@npm:0.1.12" @@ -1028,6 +1041,22 @@ __metadata: languageName: node linkType: hard +"@goodwidget/streaming-widget@npm:0.1.1": + version: 0.1.1 + resolution: "@goodwidget/streaming-widget@npm:0.1.1" + dependencies: + "@goodsdks/streaming-sdk": "npm:1.1.0" + "@goodwidget/core": "npm:0.1.5" + "@goodwidget/embed": "npm:0.1.4" + "@goodwidget/ui": "npm:0.1.6" + viem: "npm:^2.0.0" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/736eed474360e938a07e8a4d27dafabe0baa49e0dcd9323da2370cd73a719a01f1ade4363a7e2d72a635cc85de309baa3e40b00a96b79702e2657e20d70f98bd + languageName: node + linkType: hard + "@goodwidget/superfluid-campaign-widget@npm:0.1.7": version: 0.1.7 resolution: "@goodwidget/superfluid-campaign-widget@npm:0.1.7" @@ -1119,6 +1148,15 @@ __metadata: languageName: node linkType: hard +"@graphql-typed-document-node/core@npm:^3.2.0": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10c0/94e9d75c1f178bbae8d874f5a9361708a3350c8def7eaeb6920f2c820e82403b7d4f55b3735856d68e145e86c85cbfe2adc444fdc25519cd51f108697e99346c + languageName: node + linkType: hard + "@hcaptcha/react-hcaptcha@npm:^2.0.2": version: 2.0.2 resolution: "@hcaptcha/react-hcaptcha@npm:2.0.2" @@ -3160,6 +3198,27 @@ __metadata: languageName: node linkType: hard +"@sfpro/sdk@npm:^0.1.0": + version: 0.1.9 + resolution: "@sfpro/sdk@npm:0.1.9" + peerDependencies: + "@wagmi/core": ^2 + react: ">=18" + viem: ^2 + wagmi: ^2 + peerDependenciesMeta: + "@wagmi/core": + optional: true + react: + optional: true + viem: + optional: true + wagmi: + optional: true + checksum: 10c0/b800a428948d18674fb9ca2a1cec013cd56cdfd844f9b859249d7c174d1f4f99a2eafe74f542f4c11122c7c049389f68d5bbdc72278bcbbbb89586379034abe7 + languageName: node + linkType: hard + "@signinwithethereum/siwe-parser@npm:^4.2.0": version: 4.2.0 resolution: "@signinwithethereum/siwe-parser@npm:4.2.0" @@ -10050,6 +10109,7 @@ __metadata: "@goodwidget/core": "npm:0.1.5" "@goodwidget/embed": "npm:0.1.4" "@goodwidget/goodreserve-widget": "npm:0.1.7" + "@goodwidget/streaming-widget": "npm:0.1.1" "@goodwidget/superfluid-campaign-widget": "npm:0.1.7" "@goodwidget/ui": "npm:0.1.6" "@hcaptcha/react-hcaptcha": "npm:^2.0.2" @@ -10132,6 +10192,24 @@ __metadata: languageName: node linkType: hard +"graphql-request@npm:^7.1.2": + version: 7.4.0 + resolution: "graphql-request@npm:7.4.0" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.2.0" + peerDependencies: + graphql: 14 - 16 + checksum: 10c0/066531d70b9c4656251e51c950ce9bee3df05291e12e2b983608d75ac3a70d700efd6488d273b043235dc125658973db38b21eba59e808863831a5a19a6a7b41 + languageName: node + linkType: hard + +"graphql@npm:^16.9.0": + version: 16.14.2 + resolution: "graphql@npm:16.14.2" + checksum: 10c0/a95a96961eaff55cc9fe9d31fae6f33499ac988b972d07ea5085024cb1333f515b902f376e7393a5489aa82200a8aff3eb96580e4d1b69d702ed19b6eb1ce97a + languageName: node + linkType: hard + "h3@npm:^1.15.10": version: 1.15.11 resolution: "h3@npm:1.15.11"