diff --git a/apps/superfluid-campaign-web/src/App.tsx b/apps/superfluid-campaign-web/src/App.tsx index c6f0b0d4..a6caf618 100644 --- a/apps/superfluid-campaign-web/src/App.tsx +++ b/apps/superfluid-campaign-web/src/App.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useRef } from 'react' import { SuperfluidCampaignWidget } from '@goodwidget/superfluid-campaign-widget' import type { EIP1193Provider } from '@goodwidget/core' +import type { CampaignActionLinkOverrides } from '@goodwidget/superfluid-campaign-widget' import { DEFAULT_APPKIT_NETWORKS, DefaultAppKitProvider, @@ -14,6 +15,22 @@ import { YStack, defaultConfig } from '@goodwidget/ui' const DESKTOP_WIDGET_MAX_WIDTH = 960 +/** + * Tracked ubi.gd destinations for the standalone app. Invite, Vote on Flow State + * and Fund GoodBuilders are intentionally absent: those cards are disabled for + * Season 7 and must not carry a link. + */ +const STANDALONE_ACTION_LINKS: CampaignActionLinkOverrides = { + 'claim-ubi': 'https://ubi.gd/4BTD7Vb', + 'gardens-donation': 'https://ubi.gd/4eunQAd', + 'gardens-funding': 'https://ubi.gd/4zEWvDW', + 'ai-credits-purchase': 'https://ubi.gd/3UFIqH3', + 'ai-credits-subscription': 'https://ubi.gd/4AslL0c', +} + +/** Tracked ubi.gd link opened by the header's "Claim SUP rewards" button. */ +const STANDALONE_CLAIM_REWARDS_URL = 'https://ubi.gd/4iN7RhK' + /** * Pressing the disconnect-menu item under AppKit opens AppKit's own Account * view (see disconnectOverride below) rather than ending the session @@ -103,14 +120,8 @@ function AppKitSuperfluidCampaignWidget() { disconnectOverride={async () => { await open({ view: 'Account' }) }} - actionLinks={{ - 'flow-state-vote': 'https://ubi.gd/4fEqvrS', - 'flow-state-funding': 'https://ubi.gd/4z2tH8y', - 'gardens-donation': 'https://ubi.gd/4xhk4kv', - 'gardens-funding': 'https://ubi.gd/3TABl9O', - 'invite-users': 'https://ubi.gd/4xhYTyH', - 'claim-ubi': 'https://ubi.gd/3RNtzJd', - }} + actionLinks={STANDALONE_ACTION_LINKS} + claimRewardsUrl={STANDALONE_CLAIM_REWARDS_URL} /> ) } @@ -131,6 +142,8 @@ function CampaignWidgetFrame() { provider={typeof window === 'undefined' ? undefined : window.ethereum} defaultTheme="dark" contentMaxWidth={DESKTOP_WIDGET_MAX_WIDTH} + actionLinks={STANDALONE_ACTION_LINKS} + claimRewardsUrl={STANDALONE_CLAIM_REWARDS_URL} /> ) } diff --git a/examples/storybook/src/stories/helpers/superfluidCampaignWidgetStories.tsx b/examples/storybook/src/stories/helpers/superfluidCampaignWidgetStories.tsx index cd7e0064..ffab777a 100644 --- a/examples/storybook/src/stories/helpers/superfluidCampaignWidgetStories.tsx +++ b/examples/storybook/src/stories/helpers/superfluidCampaignWidgetStories.tsx @@ -1,6 +1,7 @@ import React from 'react' import { SuperfluidCampaignWidget, + type CampaignActionLinkOverrides, type SuperfluidCampaignWidgetProps, type SuperfluidCampaignView, } from '@goodwidget/superfluid-campaign-widget' @@ -16,6 +17,32 @@ import { isInjectedProviderUsable, } from '../../fixtures/injectedEip1193' +/** Integrator link sets reviewed in Storybook; disabled actions intentionally carry no link. */ +export const STANDALONE_STORY_LINKS: { + actionLinks: CampaignActionLinkOverrides + claimRewardsUrl: string +} = { + claimRewardsUrl: 'https://ubi.gd/4iN7RhK', + actionLinks: { + 'claim-ubi': 'https://ubi.gd/4BTD7Vb', + 'gardens-donation': 'https://ubi.gd/4eunQAd', + 'gardens-funding': 'https://ubi.gd/4zEWvDW', + 'ai-credits-purchase': 'https://ubi.gd/3UFIqH3', + 'ai-credits-subscription': 'https://ubi.gd/4AslL0c', + }, +} + +export const GOODWALLET_STORY_LINKS: typeof STANDALONE_STORY_LINKS = { + claimRewardsUrl: 'https://ubi.gd/4sR6vY1', + actionLinks: { + 'claim-ubi': 'https://ubi.gd/3UuQxWV', + 'gardens-donation': 'https://ubi.gd/4zzqRrj', + 'gardens-funding': 'https://ubi.gd/4C02hl6', + 'ai-credits-purchase': 'https://ubi.gd/4sRkCwz', + 'ai-credits-subscription': 'https://ubi.gd/4Ap8chU', + }, +} + function StoryShell({ children, dataTestId }: { children: React.ReactNode; dataTestId: string }) { return ( @@ -34,6 +61,8 @@ interface MockStoryProps { initialView?: SuperfluidCampaignView leaderboardScenario?: MockLeaderboardScenario programSupTotalsScenario?: MockProgramSupTotalsScenario + actionLinks?: CampaignActionLinkOverrides + claimRewardsUrl?: string } function MockSuperfluidCampaignWidgetStoryShell({ @@ -43,6 +72,8 @@ function MockSuperfluidCampaignWidgetStoryShell({ initialView = 'content', leaderboardScenario, programSupTotalsScenario, + actionLinks, + claimRewardsUrl, }: MockStoryProps & { provider: unknown; dataTestId: string }) { return ( @@ -52,6 +83,8 @@ function MockSuperfluidCampaignWidgetStoryShell({ initialView={initialView} leaderboardScenario={leaderboardScenario} programSupTotalsScenario={programSupTotalsScenario} + actionLinks={actionLinks} + claimRewardsUrl={claimRewardsUrl} /> ) diff --git a/examples/storybook/src/stories/superfluid-campaign-widget/SuperfluidCampaignWidgetQA.stories.tsx b/examples/storybook/src/stories/superfluid-campaign-widget/SuperfluidCampaignWidgetQA.stories.tsx index ac8da896..a3afb091 100644 --- a/examples/storybook/src/stories/superfluid-campaign-widget/SuperfluidCampaignWidgetQA.stories.tsx +++ b/examples/storybook/src/stories/superfluid-campaign-widget/SuperfluidCampaignWidgetQA.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react' import { MockSuperfluidCampaignWidget } from '@goodwidget/superfluid-campaign-widget/mocked' import { + GOODWALLET_STORY_LINKS, + STANDALONE_STORY_LINKS, CustodialLocalFixtureStory, NoWalletLeaderboardStory, NoWalletStory, @@ -33,6 +35,28 @@ export const NoWalletLeaderboard: Story = { render: ({ defaultTheme }) => , } +// Season 7 integrator link sets: the standalone app and the GoodWallet-hosted widget +// use different tracked links for the same cards. +export const StandaloneLinksContent: Story = { + render: ({ defaultTheme }) => ( + + ), +} + +export const GoodWalletLinksContent: Story = { + render: ({ defaultTheme }) => ( + + ), +} + export const CustodialLocalFixtureContent: Story = { render: ({ defaultTheme }) => ( diff --git a/packages/superfluid-campaign-widget/src/SuperfluidCampaignWidget.tsx b/packages/superfluid-campaign-widget/src/SuperfluidCampaignWidget.tsx index 7246d0ac..6ea3e0ee 100644 --- a/packages/superfluid-campaign-widget/src/SuperfluidCampaignWidget.tsx +++ b/packages/superfluid-campaign-widget/src/SuperfluidCampaignWidget.tsx @@ -20,6 +20,8 @@ import type { CampaignActionDefinition, CampaignActionLinkOverrides, CampaignDefinition, + FaqAnswerSegment, + FaqItemDefinition, LeaderboardSummaryData, SuperfluidCampaignView, SuperfluidCampaignWidgetProps, @@ -33,6 +35,7 @@ const GOODDOLLAR_PAGE_URL = 'https://goodwallet.xyz/en/gooddollar' interface SuperfluidCampaignRuntimeProps { data: SuperfluidCampaignWidgetProps['data'] actionLinks?: CampaignActionLinkOverrides + claimRewardsUrl?: SuperfluidCampaignWidgetProps['claimRewardsUrl'] leaderboardSummary?: LeaderboardSummaryData dataClient: SuperfluidCampaignDataClient citizenClaimEnvironment: SuperfluidCampaignWidgetProps['citizenClaimEnvironment'] @@ -93,6 +96,30 @@ function handleActionCta( } } +/** Replaces the href of FAQ link segments that mirror an action card's destination. */ +function applyFaqLinkOverrides( + faq: FaqItemDefinition[], + actionLinks: CampaignActionLinkOverrides, +): FaqItemDefinition[] { + const overrideSegment = (segment: FaqAnswerSegment): FaqAnswerSegment => { + if (typeof segment === 'string' || !segment.actionLinkActivity) return segment + const href = actionLinks[segment.actionLinkActivity] + return href ? { ...segment, href } : segment + } + + return faq.map((item) => { + if (typeof item.answer === 'string') return item + return { + ...item, + answer: item.answer.map((block) => + Array.isArray(block) + ? block.map(overrideSegment) + : { ...block, items: block.items.map((bullet) => bullet.map(overrideSegment)) }, + ), + } + }) +} + function applyActionLinkOverrides( campaign: CampaignDefinition, actionLinks?: CampaignActionLinkOverrides, @@ -101,6 +128,7 @@ function applyActionLinkOverrides( return { ...campaign, + faq: applyFaqLinkOverrides(campaign.faq, actionLinks), pools: campaign.pools.map((pool) => ({ ...pool, actions: pool.actions.map((action) => { @@ -114,6 +142,7 @@ function applyActionLinkOverrides( function SuperfluidCampaignRuntime({ data, actionLinks, + claimRewardsUrl, leaderboardSummary, dataClient, citizenClaimEnvironment, @@ -155,6 +184,7 @@ function SuperfluidCampaignRuntime({ setEmbeddedClaimTab(null)} @@ -201,7 +231,7 @@ function SuperfluidCampaignRuntime({ top-right slot (see CampaignHeader) instead of its own row here. */} - - {action.description} - + + {action.description} + = { xs: 16, sm: 20 } /** * Renders the selected campaign's activity glyphs in campaign-definition order, * dimming any activity absent from completedActivities. - * Desktop keeps all six inline; below $gtMd they wrap to a second line + * Each glyph carries a shared tooltip naming the action (hover, focus, tap). + * Desktop keeps all glyphs inline; below $gtMd they wrap to a second line * within the cell instead of truncating (see LeaderboardRow's cell wrapper). */ export function ActivityIcons({ completedActivities, activities, size = 'sm' }: ActivityIconsProps) { @@ -33,13 +34,14 @@ export function ActivityIcons({ completedActivities, activities, size = 'sm' }: const color = resolveActivityIconColorToken(spec.colorVariant, isDone) return ( - - - + + + + + ) })} diff --git a/packages/superfluid-campaign-widget/src/components/CampaignHeader.tsx b/packages/superfluid-campaign-widget/src/components/CampaignHeader.tsx index cfc27e51..0762b4e1 100644 --- a/packages/superfluid-campaign-widget/src/components/CampaignHeader.tsx +++ b/packages/superfluid-campaign-widget/src/components/CampaignHeader.tsx @@ -2,20 +2,19 @@ import React from 'react' import { Badge, BadgeText, Button, ButtonText, Heading, Icon, Text, XStack, YStack } from '@goodwidget/ui' import type { CampaignDefinition } from '../widgetRuntimeContract' import { ConnectWalletPrompt } from './ConnectWalletPrompt' -import { SeasonNotice } from './SeasonNotice' import { compactButtonProps } from './shared/styles' import { WalletControls } from '@goodwidget/core' -/** claim.superfluid.org is the Superfluid-operated claim app — always opened in a new tab, never embedded. */ -const SUPERFLUID_CLAIM_APP_URL = 'https://claim.superfluid.org/' +/** claim.superfluid.org is the Superfluid-operated claim app — the default Claim SUP destination, always opened in a new tab, never embedded. */ +const DEFAULT_CLAIM_REWARDS_URL = 'https://claim.superfluid.org/' interface CampaignHeaderProps { data: Pick< CampaignDefinition, 'seasonLabel' | 'title' | 'description' | 'supAllocatedLabel' | 'endsLabel' > - /** Callout rendered under the title; only the main content view passes it. */ - notice?: string + /** Destination of the Claim SUP rewards button; defaults to the Superfluid claim app. */ + claimRewardsUrl?: string isConnected: boolean onConnect: () => void /** Present when the campaign shell is showing an in-place child widget. */ @@ -35,7 +34,7 @@ interface CampaignHeaderProps { */ export function CampaignHeader({ data, - notice, + claimRewardsUrl = DEFAULT_CLAIM_REWARDS_URL, isConnected, onConnect, onClose, @@ -88,7 +87,6 @@ export function CampaignHeader({ {data.title} - {notice && } {data.description} @@ -109,7 +107,7 @@ export function CampaignHeader({ diff --git a/packages/superfluid-campaign-widget/src/components/LeaderboardRow.tsx b/packages/superfluid-campaign-widget/src/components/LeaderboardRow.tsx index 1a93497f..07c4702a 100644 --- a/packages/superfluid-campaign-widget/src/components/LeaderboardRow.tsx +++ b/packages/superfluid-campaign-widget/src/components/LeaderboardRow.tsx @@ -1,5 +1,15 @@ import React from 'react' -import { Anchor, Badge, BadgeText, Card, Text, XStack, YStack } from '@goodwidget/ui' +import { + Badge, + BadgeText, + Button, + Card, + Icon, + Text, + XStack, + YStack, + createToast, +} from '@goodwidget/ui' import type { ActivityType, LeaderboardEntry } from '../widgetRuntimeContract' import { ActivityIcons } from './ActivityIcons' import { truncateAddress } from './shared/styles' @@ -7,7 +17,7 @@ import { truncateAddress } from './shared/styles' /** Shared by the header and rows so every table column has one stable width. */ export const LEADERBOARD_COLUMN_WIDTHS = { rank: 42, - address: 110, + address: 150, points: 100, actions: 160, } as const @@ -17,7 +27,7 @@ export const LEADERBOARD_COLUMN_WIDTHS = { * can make the available content width smaller than this, so the parent table * scrolls horizontally instead of allowing points or badges to overlap. */ -export const LEADERBOARD_ROW_MIN_WIDTH = 480 +export const LEADERBOARD_ROW_MIN_WIDTH = 520 interface LeaderboardRowProps { entry: LeaderboardEntry @@ -27,7 +37,7 @@ interface LeaderboardRowProps { } /** - * One leaderboard row: rank, linked address/ENS, points, and the active + * One leaderboard row: rank, address/ENS with a copy button, points, and the active * campaign's activity icons. * * Every viewport uses the same four inline table columns. Below $sm, the @@ -36,6 +46,16 @@ interface LeaderboardRowProps { export function LeaderboardRow({ entry, isCurrentUser = false, activities }: LeaderboardRowProps) { const addressLabel = entry.ensName ?? truncateAddress(entry.address) + // Always copies the full address, even when the row displays a truncated address or ENS name. + const handleCopyAddress = async () => { + try { + await navigator.clipboard.writeText(entry.address) + createToast({ message: 'Address copied', status: 'success' }) + } catch { + createToast({ message: 'Could not copy address', status: 'error' }) + } + } + return ( - - {addressLabel} - + {addressLabel} + {isCurrentUser && ( You diff --git a/packages/superfluid-campaign-widget/src/components/SeasonNotice.tsx b/packages/superfluid-campaign-widget/src/components/SeasonNotice.tsx deleted file mode 100644 index 89f4930d..00000000 --- a/packages/superfluid-campaign-widget/src/components/SeasonNotice.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { Icon, Text, XStack } from '@goodwidget/ui' - -interface SeasonNoticeProps { - message: string -} - -/** - * Warning-tone callout with an exclamation-mark icon, styled like the shared - * `Alert` (muted background, warning border) so season-level announcements match - * the notices used elsewhere in GoodWidget. - */ -export function SeasonNotice({ message }: SeasonNoticeProps) { - return ( - - {/* flexShrink 0 keeps the icon from being squeezed when the message wraps on narrow widths. */} - - - - - {message} - - - ) -} diff --git a/packages/superfluid-campaign-widget/src/components/activityIconComponents.ts b/packages/superfluid-campaign-widget/src/components/activityIconComponents.ts index 64f33e8b..2b38fbd4 100644 --- a/packages/superfluid-campaign-widget/src/components/activityIconComponents.ts +++ b/packages/superfluid-campaign-widget/src/components/activityIconComponents.ts @@ -1,4 +1,4 @@ -import { CalendarDays, HandCoins, Megaphone, UserPlus, Waves } from '@goodwidget/ui' +import { CalendarDays, HandCoins, Megaphone, ShoppingCart, UserPlus, Waves } from '@goodwidget/ui' import type { ActivityType } from '../widgetRuntimeContract' /** Exact glyph mapping from the approved design reference (#127) — do not substitute. */ @@ -9,6 +9,8 @@ export const ACTIVITY_ICON_COMPONENT: Record 'flow-state-funding': Waves, 'gardens-donation': HandCoins, 'gardens-funding': Waves, + 'ai-credits-purchase': ShoppingCart, + 'ai-credits-subscription': Waves, } /** Resolves an activity's done-state to the color token its lucide icon should render in. */ diff --git a/packages/superfluid-campaign-widget/src/element.ts b/packages/superfluid-campaign-widget/src/element.ts index b1252cfb..273d377b 100644 --- a/packages/superfluid-campaign-widget/src/element.ts +++ b/packages/superfluid-campaign-widget/src/element.ts @@ -16,7 +16,7 @@ import type { SuperfluidCampaignWidgetProps } from './widgetRuntimeContract' * const el = document.querySelector('gw-superfluid-campaign') * el.provider = window.ethereum * el.initialView = 'leaderboard' - * el.poolAddresses = { 606: '0xYourPoolAddress' } + * el.poolAddresses = { 701: '0xYourPoolAddress' } * el.themeOverrides = { tokens: { color: { primary: '#00AFFE' } } } */ const SuperfluidCampaignWidgetElementBase = createMiniAppElement( @@ -28,6 +28,7 @@ const SuperfluidCampaignWidgetElementBase = createMiniAppElement( actionLinks: 'property', citizenClaimEnvironment: 'property', citizenClaimExecution: 'property', + claimRewardsUrl: 'property', disableClaim: 'property', connectOverride: 'property', contentMaxWidth: 'property', @@ -45,6 +46,7 @@ export class SuperfluidCampaignWidgetElement extends SuperfluidCampaignWidgetEle declare actionLinks: SuperfluidCampaignWidgetProps['actionLinks'] declare citizenClaimEnvironment: SuperfluidCampaignWidgetProps['citizenClaimEnvironment'] declare citizenClaimExecution: SuperfluidCampaignWidgetProps['citizenClaimExecution'] + declare claimRewardsUrl: SuperfluidCampaignWidgetProps['claimRewardsUrl'] declare disableClaim: SuperfluidCampaignWidgetProps['disableClaim'] declare connectOverride: SuperfluidCampaignWidgetProps['connectOverride'] declare contentMaxWidth: SuperfluidCampaignWidgetProps['contentMaxWidth'] diff --git a/packages/superfluid-campaign-widget/src/hooks/useProgramSupTotals.ts b/packages/superfluid-campaign-widget/src/hooks/useProgramSupTotals.ts index 1948f8ee..a0feb021 100644 --- a/packages/superfluid-campaign-widget/src/hooks/useProgramSupTotals.ts +++ b/packages/superfluid-campaign-widget/src/hooks/useProgramSupTotals.ts @@ -14,7 +14,7 @@ export interface ProgramSupTotals { export interface ProgramSupTotalsResult { /** Null when the campaign has no on-chain program registered yet (a normal, - * handled case — e.g. Ecosystem funding actions/614 as of Season 6 launch — not an error). */ + * handled case — e.g. a Season 7 program that Superfluid has not published yet — not an error). */ data: ProgramSupTotals | null isLoading: boolean error: string | null diff --git a/packages/superfluid-campaign-widget/src/mocked/fixtures.ts b/packages/superfluid-campaign-widget/src/mocked/fixtures.ts index 15ff6116..ee1bbb65 100644 --- a/packages/superfluid-campaign-widget/src/mocked/fixtures.ts +++ b/packages/superfluid-campaign-widget/src/mocked/fixtures.ts @@ -24,13 +24,13 @@ export const DEFAULT_MOCK_SUPERFLUID_CAMPAIGN_SCENARIO: MockSuperfluidCampaignSc export const MOCK_LEADERBOARD_SUMMARY: LeaderboardSummaryData = { totalParticipants: 2184, supDistributed: 316300, - supTotal: 622000, + supTotal: 1345065, lastUpdatedLabel: 'Last updated: 18m ago', } export const MOCK_CAMPAIGN_DEFINITION: CampaignDefinition = { ...DEFAULT_CAMPAIGN_DEFINITION, - supAllocatedLabel: '622K SUP allocated', + supAllocatedLabel: '1.35M SUP allocated', } const AIRDROP_STATUS_FIXTURES = { @@ -67,11 +67,11 @@ const LEADERBOARD_DATA_FIXTURES: Record< number, NonNullable['data']> > = { - 606: { + 701: { summary: { - campaignId: 606, + campaignId: 701, name: 'GoodDollar Actions', - slug: 'good-dollar-actions', + slug: 's7-gooddollar', totalPoints: 128450, memberCount: 624, totalEvents: 3891, @@ -84,14 +84,14 @@ const LEADERBOARD_DATA_FIXTURES: Record< totalPoints: 4820, eventCount: 96, lastEventAt: '2026-07-29T12:00:00.000Z', - completedActivities: ['claim-ubi', 'invite-users', 'flow-state-vote'], + completedActivities: ['claim-ubi'], }, { account: '0x2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c', totalPoints: 4390, eventCount: 88, lastEventAt: '2026-07-29T11:00:00.000Z', - completedActivities: ['claim-ubi', 'invite-users'], + completedActivities: ['claim-ubi'], }, { account: '0x3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d', @@ -110,11 +110,11 @@ const LEADERBOARD_DATA_FIXTURES: Record< hasPrevPage: false, }, }, - 614: { + 710: { summary: { - campaignId: 614, + campaignId: 710, name: 'Ecosystem Contributions', - slug: 'ecosystem-funding-actions', + slug: 's7-gooddollar-2', totalPoints: 84200, memberCount: 318, totalEvents: 1745, @@ -127,14 +127,19 @@ const LEADERBOARD_DATA_FIXTURES: Record< totalPoints: 3420, eventCount: 55, lastEventAt: '2026-07-29T09:00:00.000Z', - completedActivities: ['flow-state-funding', 'gardens-donation', 'gardens-funding'], + completedActivities: [ + 'gardens-donation', + 'gardens-funding', + 'ai-credits-purchase', + 'ai-credits-subscription', + ], }, { account: '0x5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f', totalPoints: 2985, eventCount: 47, lastEventAt: '2026-07-29T08:00:00.000Z', - completedActivities: ['gardens-donation'], + completedActivities: ['gardens-donation', 'ai-credits-purchase'], }, ], pagination: { @@ -156,8 +161,8 @@ const SUP_TOTALS_FIXTURES: Record< number, NonNullable['data']> > = { - 606: { totalAllocated: 217700, totalClaimed: 128940, totalMembers: 712 }, - 614: { totalAllocated: 404300, totalClaimed: 262450, totalMembers: 318 }, + 701: { totalAllocated: 470772.75, totalClaimed: 128940, totalMembers: 712 }, + 710: { totalAllocated: 874292.25, totalClaimed: 262450, totalMembers: 318 }, } /** Named leaderboard scenarios exercised by Storybook and Playwright. */ diff --git a/packages/superfluid-campaign-widget/src/widgetRuntimeContract.ts b/packages/superfluid-campaign-widget/src/widgetRuntimeContract.ts index 8f75a59e..4c5e47ea 100644 --- a/packages/superfluid-campaign-widget/src/widgetRuntimeContract.ts +++ b/packages/superfluid-campaign-widget/src/widgetRuntimeContract.ts @@ -17,7 +17,8 @@ export type SuperfluidCampaignWidgetEnvironment = 'production' | 'staging' | 'de export type SuperfluidCampaignView = 'content' | 'leaderboard' // --------------------------------------------------------------------------- -// The six eligible activity types (#127 "Activity icons" section). +// The eight eligible activity types (#127 "Activity icons" section, plus the +// AntSeed AI Credits purchase and subscription actions added for Season 7). // Exact icon glyph/color mapping is fixed by the approved spec, not themeable. // --------------------------------------------------------------------------- export type ActivityType = @@ -27,6 +28,8 @@ export type ActivityType = | 'flow-state-funding' | 'gardens-donation' | 'gardens-funding' + | 'ai-credits-purchase' + | 'ai-credits-subscription' export type ActivityIconColorVariant = 'blue' | 'green' @@ -35,7 +38,7 @@ export interface ActivityIconSpec { /** Icon glyph name, resolved against @goodwidget/ui's Icon registry. */ iconName: string colorVariant: ActivityIconColorVariant - /** Accessible label, also used as a tooltip on the done/not-done glyph. */ + /** Accessible label, also shown as the tooltip on the done/not-done glyph. */ label: string } @@ -51,31 +54,43 @@ export const ACTIVITY_ICON_MAP: Record = { activity: 'invite-users', iconName: 'person-plus', colorVariant: 'blue', - label: 'Successful invite', + label: 'Invite users', }, 'flow-state-vote': { activity: 'flow-state-vote', iconName: 'megaphone', colorVariant: 'blue', - label: 'Flow State vote', + label: 'Vote on Flow State', }, 'flow-state-funding': { activity: 'flow-state-funding', iconName: 'stream', colorVariant: 'blue', - label: 'Flow State funding stream', + label: 'Fund GoodBuilders Season 5', }, 'gardens-donation': { activity: 'gardens-donation', iconName: 'hand-coin', colorVariant: 'green', - label: 'Gardens one-time donation', + label: 'Make a one-time donation', }, 'gardens-funding': { activity: 'gardens-funding', iconName: 'stream', colorVariant: 'green', - label: 'Gardens funding stream', + label: 'Stream to a Community Pool', + }, + 'ai-credits-purchase': { + activity: 'ai-credits-purchase', + iconName: 'shopping-cart', + colorVariant: 'blue', + label: 'Buy AI Credits', + }, + 'ai-credits-subscription': { + activity: 'ai-credits-subscription', + iconName: 'stream', + colorVariant: 'blue', + label: 'Subscribe to AI Credits', }, } @@ -155,6 +170,12 @@ export interface LeaderboardSummaryData { export interface FaqAnswerLinkSegment { text: string href: string + /** + * When set, the `actionLinks` override for this activity replaces `href`, so a + * link that mirrors an action card (e.g. "Learn more and get started" for Buy AI + * Credits) uses the same integrator-specific destination as that card. + */ + actionLinkActivity?: ActivityType } export type FaqAnswerSegment = string | FaqAnswerLinkSegment @@ -180,8 +201,6 @@ export interface CampaignDefinition { /** Optional fixed campaign copy; mock fixtures use this for visual-state coverage. */ supAllocatedLabel?: string endsLabel: string - /** Optional callout shown under the title in the main content view, e.g. a season-ended notice. */ - notice?: string pools: CampaignPoolDefinition[] faq: FaqItemDefinition[] } @@ -251,6 +270,11 @@ export interface SuperfluidCampaignWidgetProps { * opens the embedded CitizenClaimWidget when claiming is enabled. */ actionLinks?: CampaignActionLinkOverrides + /** + * Destination of the header's "Claim SUP rewards" button. Integrators use this + * to point at their own tracked link; defaults to https://claim.superfluid.org/. + */ + claimRewardsUrl?: string /** Passed through to the embedded CitizenClaimWidget for the Claim CTA. */ citizenClaimEnvironment?: CitizenClaimWidgetEnvironment /** Optional wallet-owned, chain-specific clients for parallel custodial claims. */ diff --git a/packages/ui/src/components/ActivityIcons.ts b/packages/ui/src/components/ActivityIcons.ts index ba1ce1cd..7e0627de 100644 --- a/packages/ui/src/components/ActivityIcons.ts +++ b/packages/ui/src/components/ActivityIcons.ts @@ -3,4 +3,4 @@ // packages import these from @goodwidget/ui rather than depending on // @tamagui/lucide-icons directly, keeping the icon library a single // dependency owned by the shared UI package. -export { CalendarDays, UserPlus, Megaphone, Waves, HandCoins } from '@tamagui/lucide-icons' +export { CalendarDays, UserPlus, Megaphone, Waves, HandCoins, ShoppingCart } from '@tamagui/lucide-icons' diff --git a/packages/ui/src/components/Tooltip.tsx b/packages/ui/src/components/Tooltip.tsx new file mode 100644 index 00000000..dae8ddee --- /dev/null +++ b/packages/ui/src/components/Tooltip.tsx @@ -0,0 +1,120 @@ +import React, { useCallback, useEffect, useId, useRef, useState } from 'react' +import { Stack, Text as TamaguiText } from 'tamagui' + +/** How long a tapped tooltip stays visible when no hover/blur event closes it (touch devices). */ +const TAP_VISIBLE_DURATION_MS = 2500 +/** Gap between the trigger and the tooltip bubble. */ +const TOOLTIP_OFFSET_PX = 8 + +export interface TooltipProps { + /** Text shown inside the tooltip bubble; also announced to assistive tech through aria-describedby. */ + label: string + /** The element the tooltip describes. */ + children: React.ReactNode +} + +interface TooltipPosition { + left: number + top: number +} + +/** + * Tooltip shown on hover, keyboard focus, and tap. + * + * The bubble is positioned with `position: fixed` from the trigger's bounding + * box instead of being portalled, so it is not clipped by horizontally + * scrolling ancestors (the leaderboard table) and stays inside the widget's + * shadow root where the theme styles live. Escape dismisses it. + */ +export function Tooltip({ label, children }: TooltipProps) { + const tooltipId = useId() + const triggerRef = useRef(null) + const tapTimeoutRef = useRef | null>(null) + const [position, setPosition] = useState(null) + + const clearTapTimeout = useCallback(() => { + if (tapTimeoutRef.current) { + clearTimeout(tapTimeoutRef.current) + tapTimeoutRef.current = null + } + }, []) + + const hideTooltip = useCallback(() => { + clearTapTimeout() + setPosition(null) + }, [clearTapTimeout]) + + // Centers the bubble horizontally over the trigger and places it above it. + const showTooltip = useCallback(() => { + const triggerRect = triggerRef.current?.getBoundingClientRect() + if (!triggerRect) return + setPosition({ + left: triggerRect.left + triggerRect.width / 2, + top: triggerRect.top - TOOLTIP_OFFSET_PX, + }) + }, []) + + const showTooltipBriefly = useCallback(() => { + clearTapTimeout() + showTooltip() + tapTimeoutRef.current = setTimeout(hideTooltip, TAP_VISIBLE_DURATION_MS) + }, [clearTapTimeout, hideTooltip, showTooltip]) + + useEffect(() => clearTapTimeout, [clearTapTimeout]) + + // Scrolling would leave the fixed bubble detached from its trigger, so close it. + useEffect(() => { + if (!position) return undefined + const handleDismiss = (event: Event) => { + if (event.type === 'keydown' && (event as KeyboardEvent).key !== 'Escape') return + hideTooltip() + } + window.addEventListener('scroll', handleDismiss, true) + window.addEventListener('keydown', handleDismiss) + return () => { + window.removeEventListener('scroll', handleDismiss, true) + window.removeEventListener('keydown', handleDismiss) + } + }, [position, hideTooltip]) + + return ( + } + tabIndex={0} + aria-describedby={position ? tooltipId : undefined} + onHoverIn={showTooltip} + onHoverOut={hideTooltip} + onFocus={showTooltip} + onBlur={hideTooltip} + onPress={showTooltipBriefly} + cursor="default" + > + {children} + {position && ( + + + {label} + + + )} + + ) +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 61acc254..66222227 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -69,6 +69,10 @@ export { useToast, } from './components/Toast' export type { ToastStatus, ToastConfig, ToastItem } from './components/Toast' + +// Tooltip — hover/focus/tap label bubble that escapes overflow clipping +export { Tooltip } from './components/Tooltip' +export type { TooltipProps } from './components/Tooltip' export { Alert } from './components-test/Alert' export { Badge, BadgeText } from './components-test/Badge' export { Drawer } from './components/Drawer' @@ -85,7 +89,7 @@ export type { IconName, IconSize, IconColor, IconProps } from './components/Icon // Activity icons — re-exported originals from @tamagui/lucide-icons for the // widget "activity" glyph spec (#127). See components/ActivityIcons.ts. -export { CalendarDays, UserPlus, Megaphone, Waves, HandCoins } from './components/ActivityIcons' +export { CalendarDays, UserPlus, Megaphone, Waves, HandCoins, ShoppingCart } from './components/ActivityIcons' // Stat icons — re-exported originals from @tamagui/lucide-icons for the glyphs // on widget stat cards. See components/StatIcons.ts. diff --git a/tests/widgets/superfluid-campaign-widget/states.spec.ts b/tests/widgets/superfluid-campaign-widget/states.spec.ts index 999afff1..76d7f2c1 100644 --- a/tests/widgets/superfluid-campaign-widget/states.spec.ts +++ b/tests/widgets/superfluid-campaign-widget/states.spec.ts @@ -7,6 +7,10 @@ const STORY_IDS = { '/iframe.html?id=qa-superfluidcampaignwidget-runtime-fixtures--no-wallet-content&viewMode=story', noWalletLeaderboard: '/iframe.html?id=qa-superfluidcampaignwidget-runtime-fixtures--no-wallet-leaderboard&viewMode=story', + standaloneLinksContent: + '/iframe.html?id=qa-superfluidcampaignwidget-runtime-fixtures--standalone-links-content&viewMode=story', + goodWalletLinksContent: + '/iframe.html?id=qa-superfluidcampaignwidget-runtime-fixtures--good-wallet-links-content&viewMode=story', custodialContent: '/iframe.html?id=qa-superfluidcampaignwidget-runtime-fixtures--custodial-local-fixture-content&viewMode=story', custodialLeaderboard: @@ -34,7 +38,7 @@ const STORY_IDS = { } as const // Top-ranked account address (truncated) for each campaign in LEADERBOARD_DATA_FIXTURES -// (superfluidCampaignWidgetStories.tsx) — 606 = GoodDollar actions (default tab), 614 = Ecosystem actions. +// (superfluidCampaignWidgetStories.tsx) — 701 = GoodDollar actions (default tab), 710 = Ecosystem actions. const GOOD_DOLLAR_ACTIONS_TOP_ADDRESS = '0x1a2b...9a0b' const ECOSYSTEM_FUNDING_ACTIONS_TOP_ADDRESS = '0x4d5e...2d3e' @@ -75,16 +79,22 @@ test('SuperfluidCampaignWidget renders disconnected content view', async ({ page await gotoStory(page, STORY_IDS.noWalletContent) await expect(page.getByText('Superfluid Ecosystem Rewards')).toBeVisible() - await expect(page.getByText('SEASON 6')).toBeVisible() + await expect(page.getByText('SEASON 7', { exact: true })).toBeVisible() // Disconnected CTA is required per #127 acceptance criteria await expect(page.getByText('Connect Wallet')).toBeVisible() // Leaderboard summary section should be present await expect(page.getByText('Leaderboard').first()).toBeVisible() // Reward pools should be listed await expect(page.getByText('How to participate')).toBeVisible() - // Season-ended notice is shown in the main content view - await expect(page.getByRole('note')).toContainText('The current Superfluid Rewards season has ended.') - await expect(page.getByRole('note')).toContainText('Season 7 is coming soon') + // The Season 6 season-ended notice is gone. + await expect(page.getByRole('note')).toHaveCount(0) + await expect(page.getByText('season has ended')).toHaveCount(0) + await expect( + page.getByText( + 'Complete eligible actions across GoodDollar, Flow State, Gardens, and AI Credits to earn SUP rewards.', + ), + ).toBeVisible() + await expect(page.getByText('Ends Feb 28th')).toBeVisible() await page.screenshot({ path: 'tests/widgets/superfluid-campaign-widget/test-results/scw-01-no-wallet-content.png', @@ -101,14 +111,13 @@ test('SuperfluidCampaignWidget renders disconnected leaderboard view', async ({ // Activity completion is derived from Points API events and must remain // visible for public leaderboard rows. await expect(page.getByLabel('Claim UBI: done').first()).toBeVisible() - await expect(page.getByLabel('Successful invite: done').first()).toBeVisible() - // Season-ended notice is limited to the main content view + await expect(page.getByLabel('Invite users: not done').first()).toBeVisible() await expect(page.getByRole('note')).toHaveCount(0) await expect(page.getByText('Rank', { exact: true })).toBeVisible() await expect(page.getByText('Address', { exact: true })).toBeVisible() await expect(page.getByText('Points', { exact: true })).toBeVisible() await expect(page.getByText('Actions', { exact: true })).toBeVisible() - await expect(page.getByLabel('Gardens one-time donation: not done')).not.toBeVisible() + await expect(page.getByLabel('Make a one-time donation: not done')).not.toBeVisible() await page.screenshot({ path: 'tests/widgets/superfluid-campaign-widget/test-results/scw-02-no-wallet-leaderboard.png', @@ -120,7 +129,7 @@ test('SuperfluidCampaignWidget renders connected content view', async ({ page }) await gotoStory(page, STORY_IDS.custodialContent) await expect(page.getByText('Superfluid Ecosystem Rewards')).toBeVisible() - await expect(page.getByText('SEASON 6')).toBeVisible() + await expect(page.getByText('SEASON 7', { exact: true })).toBeVisible() // Connect Wallet CTA must NOT appear for connected wallet await expect(page.getByText('Connect Wallet')).not.toBeVisible() // Header's top-right slot must show the truncated-address wallet chip @@ -221,7 +230,7 @@ test('SuperfluidCampaignWidget mobile leaderboard keeps its headers and scrolls await expect(page.getByText('Points', { exact: true })).toBeVisible() await expect(page.getByText('Actions', { exact: true })).toBeVisible() await expect(page.getByTestId('LeaderboardRow-1')).toHaveCSS('flex-direction', 'row') - await expect(page.getByTestId('LeaderboardRow-1')).toHaveCSS('min-width', '480px') + await expect(page.getByTestId('LeaderboardRow-1')).toHaveCSS('min-width', '520px') const scrollMetrics = await page.getByTestId('Leaderboard-table-scroll').evaluate((element) => ({ clientWidth: element.clientWidth, @@ -297,7 +306,7 @@ test("SuperfluidCampaignWidget campaign leaderboard: switching tabs shows each c }) => { await gotoStory(page, STORY_IDS.leaderboardPopulated) - // Default tab (GoodDollar actions, campaignId 606) + // Default tab (GoodDollar actions, campaignId 701) await expect(page.getByText(GOOD_DOLLAR_ACTIONS_TOP_ADDRESS)).toBeVisible() await expect(page.getByText(ECOSYSTEM_FUNDING_ACTIONS_TOP_ADDRESS)).not.toBeVisible() @@ -312,15 +321,82 @@ test("SuperfluidCampaignWidget campaign leaderboard: switching tabs shows each c }) }) -test('SuperfluidCampaignWidget leaderboard addresses link to the Base Superfluid Explorer', async ({ +test('SuperfluidCampaignWidget leaderboard addresses are plain text with a copy button that copies the full address', async ({ page, + context, }) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']) await gotoStory(page, STORY_IDS.noWalletLeaderboard) - await expect(page.getByRole('link', { name: GOOD_DOLLAR_ACTIONS_TOP_ADDRESS })).toHaveAttribute( - 'href', - 'https://explorer.superfluid.org/base-mainnet/accounts/0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b', - ) + await expect(page.getByText(GOOD_DOLLAR_ACTIONS_TOP_ADDRESS)).toBeVisible() + // The address is no longer a link to the Superfluid explorer. + await expect(page.getByRole('link', { name: GOOD_DOLLAR_ACTIONS_TOP_ADDRESS })).toHaveCount(0) + await expect(page.locator('a[href*="explorer.superfluid.org"]')).toHaveCount(0) + + const fullAddress = '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b' + await page.getByLabel(`Copy address ${fullAddress}`).click() + + await expect(page.getByText('Address copied')).toBeVisible() + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(fullAddress) + + await page.screenshot({ + path: 'tests/widgets/superfluid-campaign-widget/test-results/scw-40-leaderboard-address-copied.png', + fullPage: true, + }) +}) + +test('SuperfluidCampaignWidget leaderboard action icons show a shared tooltip on hover, focus, and tap', async ({ + page, + browser, +}) => { + await gotoStory(page, STORY_IDS.noWalletLeaderboard) + + // Hover + const claimIcon = page.getByLabel('Claim UBI: done').first() + await claimIcon.hover() + await expect(page.getByRole('tooltip')).toHaveText('Claim UBI') + await page.screenshot({ + path: 'tests/widgets/superfluid-campaign-widget/test-results/scw-41-leaderboard-tooltip-hover.png', + fullPage: true, + }) + await page.mouse.move(0, 0) + await expect(page.getByRole('tooltip')).toHaveCount(0) + + // Keyboard focus, dismissed with Escape + await claimIcon.evaluate((element) => (element.parentElement as HTMLElement).focus()) + await expect(page.getByRole('tooltip')).toHaveText('Claim UBI') + await page.keyboard.press('Escape') + await expect(page.getByRole('tooltip')).toHaveCount(0) + + // Tap on a touch device + const touchContext = await browser.newContext({ hasTouch: true }) + const touchPage = await touchContext.newPage() + await gotoStory(touchPage, STORY_IDS.noWalletLeaderboard) + await touchPage.getByLabel('Invite users: not done').first().tap() + await expect(touchPage.getByRole('tooltip')).toHaveText('Invite users') + await touchContext.close() +}) + +test('SuperfluidCampaignWidget leaderboard tooltips use the Season 7 action labels', async ({ + page, +}) => { + await gotoStory(page, STORY_IDS.leaderboardPopulated) + const goodDollarLabels = ['Claim UBI', 'Invite users', 'Vote on Flow State'] + for (const label of goodDollarLabels) { + await expect(page.getByLabel(new RegExp(`^${label}: (done|not done)$`)).first()).toBeVisible() + } + + await page.getByText('Ecosystem actions').click() + const ecosystemLabels = [ + 'Fund GoodBuilders Season 5', + 'Make a one-time donation', + 'Stream to a Community Pool', + 'Buy AI Credits', + 'Subscribe to AI Credits', + ] + for (const label of ecosystemLabels) { + await expect(page.getByLabel(new RegExp(`^${label}: (done|not done)$`)).first()).toBeVisible() + } }) test('SuperfluidCampaignWidget leaderboard only shows actions for the selected campaign', async ({ @@ -329,11 +405,13 @@ test('SuperfluidCampaignWidget leaderboard only shows actions for the selected c await gotoStory(page, STORY_IDS.leaderboardPopulated) await expect(page.getByLabel('Claim UBI: done').first()).toBeVisible() - await expect(page.getByLabel('Gardens funding stream: not done')).not.toBeVisible() + await expect(page.getByLabel('Stream to a Community Pool: not done')).not.toBeVisible() await page.getByText('Ecosystem actions').click() - await expect(page.getByLabel('Gardens funding stream: done').first()).toBeVisible() + await expect(page.getByLabel('Stream to a Community Pool: done').first()).toBeVisible() + await expect(page.getByLabel('Buy AI Credits: done').first()).toBeVisible() + await expect(page.getByLabel('Subscribe to AI Credits: done').first()).toBeVisible() await expect(page.getByLabel('Claim UBI: not done')).not.toBeVisible() }) @@ -345,9 +423,9 @@ test('SuperfluidCampaignWidget enriches a leaderboard page from per-account poin await page.route('**/points/campaign?*', async (route) => { await route.fulfill({ json: { - campaignId: 606, + campaignId: 701, name: 'GoodDollar Actions', - slug: 'good-dollar-actions', + slug: 's7-gooddollar', totalPoints: 12, memberCount: 1, totalEvents: 3, @@ -402,23 +480,97 @@ test('SuperfluidCampaignWidget enriches a leaderboard page from per-account poin await gotoStory(page, STORY_IDS.leaderboardApiContract) await expect(page.getByLabel('Claim UBI: done')).toBeVisible() - await expect(page.getByLabel('Successful invite: not done')).toBeVisible() - await expect(page.getByLabel('Flow State vote: not done')).toBeVisible() + await expect(page.getByLabel('Invite users: not done')).toBeVisible() + await expect(page.getByLabel('Vote on Flow State: not done')).toBeVisible() }) -test('SuperfluidCampaignWidget SUP distribution and members: populated from the mocked programs client for campaign 606', async ({ +test('SuperfluidCampaignWidget marks AI Credits actions done from the AntSeed point events', async ({ + page, +}) => { + const account = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const ecosystemCampaignId = '710' + + await page.route('**/points/campaign?*', async (route) => { + await route.fulfill({ + json: { + campaignId: Number(ecosystemCampaignId), + name: 'Ecosystem Actions', + slug: 's7-gooddollar-2', + totalPoints: 3, + memberCount: 1, + totalEvents: 2, + lastEventAt: '2026-11-12T00:00:00.000Z', + createdAt: '2026-11-01T00:00:00.000Z', + }, + }) + }) + await page.route('**/points/accounts?*', async (route) => { + await route.fulfill({ + json: { + accounts: [ + { + account, + totalPoints: 3, + eventCount: 2, + lastEventAt: '2026-11-12T00:00:00.000Z', + }, + ], + pagination: { + page: 1, + limit: 10, + totalDocs: 1, + totalPages: 1, + hasNextPage: false, + hasPrevPage: false, + }, + }, + }) + }) + // Only the Ecosystem campaign carries AntSeed events; the Gardens donation has no event here. + await page.route('**/points/events?*', async (route) => { + const requestedCampaignId = new URL(route.request().url()).searchParams.get('campaignId') + const events = + requestedCampaignId === ecosystemCampaignId + ? [ + { eventName: 'aiCreditsSent', points: 1 }, + { eventName: 'aiCreditsStreamed', points: 2 }, + ] + : [] + await route.fulfill({ + json: { + events, + pagination: { + page: 1, + limit: 100, + totalDocs: events.length, + totalPages: 1, + hasNextPage: false, + hasPrevPage: false, + }, + }, + }) + }) + + await gotoStory(page, STORY_IDS.leaderboardApiContract) + await page.getByText('Ecosystem actions').click() + + await expect(page.getByLabel('Buy AI Credits: done')).toBeVisible() + await expect(page.getByLabel('Subscribe to AI Credits: done')).toBeVisible() + await expect(page.getByLabel('Stream to a Community Pool: not done')).toBeVisible() +}) + +test('SuperfluidCampaignWidget SUP distribution and members: populated from the mocked programs client for campaign 701', async ({ page, }) => { await gotoStory(page, STORY_IDS.supTotalsPopulated) // Figures come from the mocked runtime client, proving changing values are // separate from the stable campaign definition. - await expect(page.getByText('128,940 / 217,700 SUP')).toBeVisible() + await expect(page.getByText('128,940 / 470,772.75 SUP')).toBeVisible() // The programs API fixture adapter supplies the member count. await expect(page.getByText('712 participants')).toBeVisible() - // 614 (Ecosystem actions) has no matching program id yet, so - // RewardPoolSection falls back to its own unchanged mock placeholder. - await expect(page.getByText('262,450 / 404,300 SUP')).toBeVisible() + // 710 (Ecosystem actions) is served by the same mocked programs client. + await expect(page.getByText('262,450 / 874,292.25 SUP')).toBeVisible() await page.screenshot({ path: 'tests/widgets/superfluid-campaign-widget/test-results/scw-17-sup-totals-populated.png', @@ -454,17 +606,17 @@ test('SuperfluidCampaignWidget SUP totals: no program renders empty totals', asy await expect(page.getByText('0 / 0 SUP')).toHaveCount(2) }) -test('SuperfluidCampaignWidget SUP distribution and members sourced from the programs API for campaign 606', async ({ +test('SuperfluidCampaignWidget SUP distribution and members sourced from the programs API for campaign 701', async ({ page, }) => { - // Mock the programs API endpoint with a minimal superjson envelope for campaign 606. + // Mock the programs API endpoint with a minimal superjson envelope for campaign 701. await page.route('https://claim.superfluid.org/api/programs', async (route) => { await route.fulfill({ json: { json: [ { program: { - id: 606, + id: 701, onchainInfo: { totalAllocated: '50000000000000000000000', totalClaimed: '1000000000000000000', @@ -488,18 +640,17 @@ test('SuperfluidCampaignWidget entire action card is clickable and triggers the page, context, }) => { - await stubExternalPopup(context, 'goodwallet.xyz') - await gotoStory(page, STORY_IDS.noWalletContent) + await stubExternalPopup(context, 'ubi.gd') + await gotoStory(page, STORY_IDS.standaloneLinksContent) // Click the card via its title text, away from the CTA button itself, to - // confirm the whole card (not just the button) is a click target. Uses - // Invite users since Vote on Flow State is now a closed/disabled action. - const cardTitle = page.getByText('Invite users').first() + // confirm the whole card (not just the button) is a click target. + const cardTitle = page.getByText('Buy AI Credits', { exact: true }).first() await expect(cardTitle).toBeVisible() const [newPage] = await Promise.all([context.waitForEvent('page'), cardTitle.click()]) await newPage.waitForLoadState('domcontentloaded').catch(() => {}) - expect(newPage.url()).toContain('goodwallet.xyz') + expect(newPage.url()).toContain('ubi.gd/3UFIqH3') await newPage.close() await page.screenshot({ @@ -512,50 +663,61 @@ test('SuperfluidCampaignWidget clicking the CTA button directly does not double- page, context, }) => { - await stubExternalPopup(context, 'goodwallet.xyz') - await gotoStory(page, STORY_IDS.noWalletContent) + await stubExternalPopup(context, 'ubi.gd') + await gotoStory(page, STORY_IDS.standaloneLinksContent) - const inviteButton = page.getByText('Invite', { exact: true }).first() - await expect(inviteButton).toBeVisible() + const subscribeButton = page.getByRole('button', { name: 'Subscribe', exact: true }) + await expect(subscribeButton).toBeVisible() const pagesBefore = context.pages().length - const [newPage] = await Promise.all([context.waitForEvent('page'), inviteButton.click()]) + const [newPage] = await Promise.all([context.waitForEvent('page'), subscribeButton.click()]) await newPage.waitForLoadState('domcontentloaded').catch(() => {}) // Give a duplicate-fire regression a moment to open a second tab before // asserting exactly one new page resulted from this single click. await page.waitForTimeout(300) expect(context.pages().length).toBe(pagesBefore + 1) - expect(newPage.url()).toContain('goodwallet.xyz') + expect(newPage.url()).toContain('ubi.gd/4AslL0c') await newPage.close() }) -test('SuperfluidCampaignWidget closed actions render disabled and non-interactive', async ({ +test('SuperfluidCampaignWidget "Coming up soon" actions render disabled, greyed and non-interactive', async ({ page, context, }) => { await gotoStory(page, STORY_IDS.noWalletContent) + const inviteCard = page.getByTestId('ActionCard-layout-invite-users') const voteCard = page.getByTestId('ActionCard-layout-flow-state-vote') const fundingCard = page.getByTestId('ActionCard-layout-flow-state-funding') + await expect(inviteCard).toBeVisible() await expect(voteCard).toBeVisible() await expect(fundingCard).toBeVisible() - await expect(voteCard.getByText('Closed', { exact: true })).toBeVisible() - await expect(fundingCard.getByText('Closed', { exact: true })).toBeVisible() - await expect(page.getByLabel('Closed: Vote on Flow State')).toHaveAttribute( + for (const card of [inviteCard, voteCard, fundingCard]) { + await expect(card.getByText('Coming up soon', { exact: true })).toBeVisible() + // Disabled actions carry no link at all. + await expect(card.locator('a[href]')).toHaveCount(0) + } + await expect(page.getByLabel('Coming up soon: Invite users')).toHaveAttribute( + 'aria-disabled', + 'true', + ) + await expect(page.getByLabel('Coming up soon: Vote on Flow State')).toHaveAttribute( 'aria-disabled', 'true', ) - await expect(page.getByLabel('Closed: Fund GoodBuilders Season 4')).toHaveAttribute( + await expect(page.getByLabel('Coming up soon: Fund GoodBuilders Season 5')).toHaveAttribute( 'aria-disabled', 'true', ) + // Points labels stay visible on disabled cards. + await expect(inviteCard.getByText(/PT/)).toBeVisible() const pagesBefore = context.pages().length - for (const card of [voteCard, fundingCard]) { + for (const card of [inviteCard, voteCard, fundingCard]) { await expect(card).not.toHaveAttribute('tabindex') - const closedButton = card.getByRole('button', { name: 'Closed' }) + const closedButton = card.getByRole('button', { name: 'Coming up soon' }) await expect(closedButton).toBeDisabled() await card.evaluate((element) => (element as HTMLElement).focus()) @@ -585,7 +747,7 @@ test('SuperfluidCampaignWidget closed actions render disabled and non-interactiv await page.keyboard.press('Tab') expect( await page.evaluate( - () => document.activeElement?.closest('[aria-label^="Closed:"]') !== null, + () => document.activeElement?.closest('[aria-label^="Coming up soon:"]') !== null, ), ).toBe(false) } @@ -618,7 +780,9 @@ test('SuperfluidCampaignWidget Gardens Donate and Fund actions open Gardens', as } }) -test('SuperfluidCampaignWidget claim keeps the Superfluid header and can be closed', async ({ page }) => { +test('SuperfluidCampaignWidget claim keeps the Superfluid header and can be closed', async ({ + page, +}) => { await gotoStory(page, STORY_IDS.noWalletContent) await page.getByTestId('ActionCard-layout-claim-ubi').getByText('Claim', { exact: true }).click() @@ -631,21 +795,198 @@ test('SuperfluidCampaignWidget claim keeps the Superfluid header and can be clos await expect(page.getByTestId('ActionCard-layout-claim-ubi')).toBeVisible() }) -test('SuperfluidCampaignWidget invite opens GoodWallet instead of the Citizen Claim widget', async ({ +test('SuperfluidCampaignWidget invite is disabled with no link and keeps its points label', async ({ page, context, }) => { - await stubExternalPopup(context, 'goodwallet.xyz') - await gotoStory(page, STORY_IDS.noWalletContent) + await gotoStory(page, STORY_IDS.goodWalletLinksContent) const inviteCard = page.getByTestId('ActionCard-layout-invite-users') - const [newPage] = await Promise.all([context.waitForEvent('page'), inviteCard.click()]) - await newPage.waitForLoadState('domcontentloaded').catch(() => {}) - expect(newPage.url()).toContain('goodwallet.xyz/en/gooddollar') - await newPage.close() + await expect(inviteCard.getByRole('button', { name: 'Coming up soon' })).toBeDisabled() + await expect(inviteCard.locator('a[href]')).toHaveCount(0) + + const pagesBefore = context.pages().length + await inviteCard.click({ force: true }) + await page.waitForTimeout(100) + expect(context.pages().length).toBe(pagesBefore) await expect(page.getByLabel('Close claim widget')).not.toBeVisible() }) +test('SuperfluidCampaignWidget AI Credits cards show the S7 copy, BUY label and no minimum caption', async ({ + page, +}) => { + await gotoStory(page, STORY_IDS.noWalletContent) + + const buyCard = page.getByTestId('ActionCard-layout-ai-credits-purchase') + await expect(buyCard.getByText('Get 10% more AI credits with G$.')).toBeVisible() + await expect(buyCard.getByText('1 PT PER 1K G$ (≈ $0.1)')).toBeVisible() + await expect(buyCard.getByRole('button', { name: 'BUY', exact: true })).toBeEnabled() + + const subscribeCard = page.getByTestId('ActionCard-layout-ai-credits-subscription') + await expect(subscribeCard.getByText('Stream G$ for 20% more AI credits.')).toBeVisible() + await expect(subscribeCard.getByText('2 PTS PER 1K G$ (≈ $0.1)')).toBeVisible() + await expect(subscribeCard.getByRole('button', { name: 'Subscribe', exact: true })).toBeEnabled() + + // The minimum-deposit details live in the FAQ, not on the cards. + await expect(buyCard.getByText(/Minimum/)).toHaveCount(0) + await expect(subscribeCard.getByText(/Minimum/)).toHaveCount(0) + + await page.screenshot({ + path: 'tests/widgets/superfluid-campaign-widget/test-results/scw-42-ai-credits-cards.png', + fullPage: true, + }) +}) + +test('SuperfluidCampaignWidget Buy and Subscribe lead the Ecosystem actions', async ({ page }) => { + await gotoStory(page, STORY_IDS.noWalletContent) + + const actionLayouts = page.locator('[data-testid^="ActionCard-layout-"]') + const layoutTestIds = await actionLayouts.evaluateAll((layouts) => + layouts.map((layout) => layout.getAttribute('data-testid')), + ) + // The first three cards belong to GoodDollar actions; the Ecosystem pool follows. + expect(layoutTestIds.slice(3, 5)).toEqual([ + 'ActionCard-layout-ai-credits-purchase', + 'ActionCard-layout-ai-credits-subscription', + ]) +}) + +test('SuperfluidCampaignWidget FAQ holds the AI Credits minimum deposit details', async ({ + page, +}) => { + await gotoStory(page, STORY_IDS.noWalletContent) + await page.getByText('FAQ', { exact: true }).first().click() + await page.getByText('How can I earn points by buying AI Credits?', { exact: true }).click() + await expect( + page.getByText('Minimum first deposit: $1. No minimum for subsequent deposits.', { + exact: false, + }), + ).toBeVisible() +}) + +test('SuperfluidCampaignWidget Season 7 card copy for Vote on Flow State and Fund GoodBuilders', async ({ + page, +}) => { + await gotoStory(page, STORY_IDS.noWalletContent) + + await expect(page.getByText('Vote to help allocate GoodBuilders Season 5 funding.')).toBeVisible() + await expect(page.getByText('Fund GoodBuilders Season 5', { exact: true }).first()).toBeVisible() + await expect(page.getByText('Start a stream to the GoodBuilders Season 5 pool.')).toBeVisible() + await expect(page.getByText('Season 4')).toHaveCount(0) +}) + +test('SuperfluidCampaignWidget FAQ reflects the Season 7 questions', async ({ page }) => { + await gotoStory(page, STORY_IDS.noWalletContent) + await page.getByText('FAQ', { exact: true }).first().click() + + const expectedQuestions = [ + 'Can I earn points for inviting users?', + 'How can I earn points by buying AI Credits?', + 'What can I do with AI Credits?', + 'Why are some actions marked ‘Coming up soon’?', + ] + for (const question of expectedQuestions) { + await expect(page.getByText(question, { exact: true })).toBeVisible() + } + const removedQuestions = [ + 'Why are Vote on Flow State and Funding closed?', + 'When did Ecosystem action points restart?', + 'Are Ecosystem actions still earning points?', + 'How do Flow State voting points work?', + ] + for (const question of removedQuestions) { + await expect(page.getByText(question, { exact: true })).toHaveCount(0) + } + + await page.getByText('Can I earn points for inviting users?', { exact: true }).click() + await expect( + page.getByText( + 'Invite rewards are coming up soon. Invites are currently not eligible for Season 7 points.', + ), + ).toBeVisible() +}) + +const LINK_STORIES = [ + { + key: 'standalone', + storyUrl: STORY_IDS.standaloneLinksContent, + claimSup: 'https://ubi.gd/4iN7RhK', + buy: 'https://ubi.gd/3UFIqH3', + subscribe: 'https://ubi.gd/4AslL0c', + donate: 'https://ubi.gd/4eunQAd', + stream: 'https://ubi.gd/4zEWvDW', + claimUbi: 'https://ubi.gd/4BTD7Vb', + screenshotIndex: 43, + }, + { + key: 'goodwallet', + storyUrl: STORY_IDS.goodWalletLinksContent, + claimSup: 'https://ubi.gd/4sR6vY1', + buy: 'https://ubi.gd/4sRkCwz', + subscribe: 'https://ubi.gd/4Ap8chU', + donate: 'https://ubi.gd/4zzqRrj', + stream: 'https://ubi.gd/4C02hl6', + claimUbi: 'https://ubi.gd/3UuQxWV', + screenshotIndex: 44, + }, +] + +for (const linkStory of LINK_STORIES) { + test(`SuperfluidCampaignWidget ${linkStory.key} version uses its own tracking links`, async ({ + page, + context, + }) => { + await stubExternalPopup(context, 'ubi.gd') + await gotoStory(page, linkStory.storyUrl) + await page.getByText('FAQ', { exact: true }).first().click() + + // Claim SUP CTA opens the integrator's tracked URL. + const [claimSupPage] = await Promise.all([ + context.waitForEvent('page'), + page.getByText('Claim SUP rewards').first().click(), + ]) + await claimSupPage.waitForLoadState('domcontentloaded').catch(() => {}) + expect(claimSupPage.url()).toContain(linkStory.claimSup.replace('https://', '')) + await claimSupPage.close() + + // The FAQ "Learn more and get started" link mirrors the Buy AI Credits link. + await page.getByText('What can I do with AI Credits?', { exact: true }).click() + const learnMoreLink = page.getByRole('link', { name: 'Learn more and get started' }) + await expect(learnMoreLink).toHaveAttribute('href', linkStory.buy) + + await page.screenshot({ + path: `tests/widgets/superfluid-campaign-widget/test-results/scw-${linkStory.screenshotIndex}-${linkStory.key}-links-content.png`, + fullPage: true, + }) + }) + + for (const [actionName, actionTestId, buttonName, expectedUrl] of [ + ['Buy AI Credits', 'ai-credits-purchase', 'BUY', linkStory.buy], + ['Subscribe', 'ai-credits-subscription', 'Subscribe', linkStory.subscribe], + ['Donate', 'gardens-donation', 'Donate', linkStory.donate], + ['Stream', 'gardens-funding', 'Fund', linkStory.stream], + ] as const) { + test(`SuperfluidCampaignWidget ${linkStory.key} ${actionName} card and button open ${expectedUrl}`, async ({ + page, + context, + }) => { + await stubExternalPopup(context, 'ubi.gd') + await gotoStory(page, linkStory.storyUrl) + + const layout = page.getByTestId(`ActionCard-layout-${actionTestId}`) + for (const trigger of [ + layout.getByRole('button', { name: buttonName, exact: true }), + layout, + ]) { + const [newPage] = await Promise.all([context.waitForEvent('page'), trigger.click()]) + await newPage.waitForLoadState('domcontentloaded').catch(() => {}) + expect(newPage.url()).toContain(expectedUrl.replace('https://', '')) + await newPage.close() + } + }) + } +} + // Narrow-viewport regression suite for the header "Connect wallet" CTA, covering // both Content view's CampaignHeader and Leaderboard view's own (separately // coded) header row. Reuses the change-request-7 breakpoint set (min-supported @@ -700,6 +1041,47 @@ const HEADER_WRAP_VIEWS = [ }, ] +// CI and local machines render the monospace font with slightly different line metrics, +// shifting otherwise-equal card heights by a pixel or two. +const CARD_HEIGHT_FONT_METRIC_TOLERANCE_PX = 4 + +/** + * The AI Credits cards must never be taller than the tallest other action card + * (so they never stand out). In the desktop row layout, where every card has a + * one-line description, they must also not be shorter than the shortest card. + * Stacked phone layouts size cards to their own copy, so only the upper bound applies. + */ +async function expectAiCreditsCardsMatchOtherCardHeights( + page: Page, + { isDesktopRow }: { isDesktopRow: boolean }, +) { + const cardHeightsByActivity = await page.evaluate(() => { + const heights: Record = {} + for (const layout of document.querySelectorAll('[data-testid^="ActionCard-layout-"]')) { + const activity = (layout.getAttribute('data-testid') ?? '').replace('ActionCard-layout-', '') + // The layout sits inside the card, whose role=button element is the visible box. + const cardElement = layout.closest('[role="button"]') + heights[activity] = cardElement?.getBoundingClientRect().height ?? 0 + } + return heights + }) + const otherCardHeights = Object.entries(cardHeightsByActivity) + .filter(([activity]) => !activity.startsWith('ai-credits')) + .map(([, height]) => height) + const minOtherHeight = Math.min(...otherCardHeights) + const maxOtherHeight = Math.max(...otherCardHeights) + for (const activity of ['ai-credits-purchase', 'ai-credits-subscription']) { + if (isDesktopRow) { + expect(cardHeightsByActivity[activity]).toBeGreaterThanOrEqual( + minOtherHeight - CARD_HEIGHT_FONT_METRIC_TOLERANCE_PX, + ) + } + expect(cardHeightsByActivity[activity]).toBeLessThanOrEqual( + maxOtherHeight + CARD_HEIGHT_FONT_METRIC_TOLERANCE_PX, + ) + } +} + for (const { width, label } of HEADER_WRAP_BREAKPOINTS) { test(`SuperfluidCampaignWidget action cards stack cleanly at ${width}px (${label})`, async ({ page, @@ -708,7 +1090,7 @@ for (const { width, label } of HEADER_WRAP_BREAKPOINTS) { await gotoStory(page, STORY_IDS.noWalletContent) const actionLayouts = page.locator('[data-testid^="ActionCard-layout-"]') - await expect(actionLayouts).toHaveCount(6) + await expect(actionLayouts).toHaveCount(8) // Every supported phone width is below the shared 480px $sm breakpoint. // The description and action footer must therefore stack instead of @@ -724,9 +1106,11 @@ for (const { width, label } of HEADER_WRAP_BREAKPOINTS) { const documentScrollWidth = await page.evaluate(() => document.documentElement.scrollWidth) expect(documentScrollWidth).toBeLessThanOrEqual(width + 1) + await expectAiCreditsCardsMatchOtherCardHeights(page, { isDesktopRow: false }) + // No CTA may be silently cut off by the card's rounded overflow boundary. const clippedButtons = await findClippedButtons(page) - for (const actionLabel of ['Claim', 'Invite', 'Vote', 'Fund', 'Donate']) { + for (const actionLabel of ['Claim', 'Coming up soon', 'BUY', 'Subscribe', 'Donate']) { expect(clippedButtons).not.toContain(actionLabel) } }) @@ -739,10 +1123,11 @@ test('SuperfluidCampaignWidget action cards retain their desktop row above 480px await gotoStory(page, STORY_IDS.noWalletContent) const actionLayouts = page.locator('[data-testid^="ActionCard-layout-"]') - await expect(actionLayouts).toHaveCount(6) + await expect(actionLayouts).toHaveCount(8) for (const layout of await actionLayouts.all()) { await expect(layout).toHaveCSS('flex-direction', 'row') } + await expectAiCreditsCardsMatchOtherCardHeights(page, { isDesktopRow: true }) }) for (const view of HEADER_WRAP_VIEWS) { diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-01-no-wallet-content.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-01-no-wallet-content.png index 066989de..a28d7722 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-01-no-wallet-content.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-01-no-wallet-content.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-02-no-wallet-leaderboard.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-02-no-wallet-leaderboard.png index f829e419..0c4ed927 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-02-no-wallet-leaderboard.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-02-no-wallet-leaderboard.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-03-custodial-content.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-03-custodial-content.png index c78a2857..df8edc67 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-03-custodial-content.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-03-custodial-content.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-04-custodial-leaderboard.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-04-custodial-leaderboard.png index 2e72c486..c28ed380 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-04-custodial-leaderboard.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-04-custodial-leaderboard.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-05-faq-expanded.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-05-faq-expanded.png index 7e9e078a..23b6ebba 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-05-faq-expanded.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-05-faq-expanded.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-06-leaderboard-navigation.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-06-leaderboard-navigation.png index f829e419..0c4ed927 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-06-leaderboard-navigation.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-06-leaderboard-navigation.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-07-mobile-content.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-07-mobile-content.png index 992f4a2a..df8964f5 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-07-mobile-content.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-07-mobile-content.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-08-wide-content.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-08-wide-content.png index fb737c04..6661fd43 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-08-wide-content.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-08-wide-content.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-09-claim-cta.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-09-claim-cta.png index 4e92c1d9..b0c02fea 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-09-claim-cta.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-09-claim-cta.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-14-leaderboard-loading.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-14-leaderboard-loading.png index fc86014e..54911958 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-14-leaderboard-loading.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-14-leaderboard-loading.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-15-leaderboard-request-failed.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-15-leaderboard-request-failed.png index 1db55664..b20a2edd 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-15-leaderboard-request-failed.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-15-leaderboard-request-failed.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-16-leaderboard-tab-switch.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-16-leaderboard-tab-switch.png index 1f9acf44..ad075361 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-16-leaderboard-tab-switch.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-16-leaderboard-tab-switch.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-17-sup-totals-populated.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-17-sup-totals-populated.png index 066989de..a28d7722 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-17-sup-totals-populated.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-17-sup-totals-populated.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-18-sup-totals-request-failed.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-18-sup-totals-request-failed.png index 6dfd1831..1050436e 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-18-sup-totals-request-failed.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-18-sup-totals-request-failed.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-19-action-card-whole-card-click.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-19-action-card-whole-card-click.png index 066989de..a28d7722 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-19-action-card-whole-card-click.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-19-action-card-whole-card-click.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-20-responsive-content-320px-min-supported.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-20-responsive-content-320px-min-supported.png index 6911e15b..58a5d2f5 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-20-responsive-content-320px-min-supported.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-20-responsive-content-320px-min-supported.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-21-responsive-content-360px-common-android.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-21-responsive-content-360px-common-android.png index ad51c5fa..7024328e 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-21-responsive-content-360px-common-android.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-21-responsive-content-360px-common-android.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-22-responsive-content-375px-iphone-se.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-22-responsive-content-375px-iphone-se.png index dbd8a5bd..1dcab1e0 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-22-responsive-content-375px-iphone-se.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-22-responsive-content-375px-iphone-se.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-23-responsive-content-390px-modern-iphone.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-23-responsive-content-390px-modern-iphone.png index 6ca382cc..27e90451 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-23-responsive-content-390px-modern-iphone.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-23-responsive-content-390px-modern-iphone.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-24-responsive-content-412px-larger-android.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-24-responsive-content-412px-larger-android.png index df889b95..74c7c533 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-24-responsive-content-412px-larger-android.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-24-responsive-content-412px-larger-android.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-25-responsive-leaderboard-disconnected-320px-min-supported.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-25-responsive-leaderboard-disconnected-320px-min-supported.png index ae3cda81..e1e97513 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-25-responsive-leaderboard-disconnected-320px-min-supported.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-25-responsive-leaderboard-disconnected-320px-min-supported.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-26-responsive-leaderboard-disconnected-360px-common-android.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-26-responsive-leaderboard-disconnected-360px-common-android.png index 92e26ff2..374c2abd 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-26-responsive-leaderboard-disconnected-360px-common-android.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-26-responsive-leaderboard-disconnected-360px-common-android.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-27-responsive-leaderboard-disconnected-375px-iphone-se.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-27-responsive-leaderboard-disconnected-375px-iphone-se.png index 640f4a63..00419ef2 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-27-responsive-leaderboard-disconnected-375px-iphone-se.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-27-responsive-leaderboard-disconnected-375px-iphone-se.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-28-responsive-leaderboard-disconnected-390px-modern-iphone.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-28-responsive-leaderboard-disconnected-390px-modern-iphone.png index 0e030094..f9d2da5e 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-28-responsive-leaderboard-disconnected-390px-modern-iphone.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-28-responsive-leaderboard-disconnected-390px-modern-iphone.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-29-responsive-leaderboard-disconnected-412px-larger-android.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-29-responsive-leaderboard-disconnected-412px-larger-android.png index e8b94a28..d2a1580a 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-29-responsive-leaderboard-disconnected-412px-larger-android.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-29-responsive-leaderboard-disconnected-412px-larger-android.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-30-responsive-leaderboard-connected-320px-min-supported.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-30-responsive-leaderboard-connected-320px-min-supported.png index afba6d5f..d6b46524 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-30-responsive-leaderboard-connected-320px-min-supported.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-30-responsive-leaderboard-connected-320px-min-supported.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-31-responsive-leaderboard-connected-360px-common-android.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-31-responsive-leaderboard-connected-360px-common-android.png index 626f6c92..b9ac21b9 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-31-responsive-leaderboard-connected-360px-common-android.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-31-responsive-leaderboard-connected-360px-common-android.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-32-responsive-leaderboard-connected-375px-iphone-se.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-32-responsive-leaderboard-connected-375px-iphone-se.png index ce16fe06..adedc790 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-32-responsive-leaderboard-connected-375px-iphone-se.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-32-responsive-leaderboard-connected-375px-iphone-se.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-33-responsive-leaderboard-connected-390px-modern-iphone.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-33-responsive-leaderboard-connected-390px-modern-iphone.png index a2526164..eaeb493d 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-33-responsive-leaderboard-connected-390px-modern-iphone.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-33-responsive-leaderboard-connected-390px-modern-iphone.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-34-responsive-leaderboard-connected-412px-larger-android.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-34-responsive-leaderboard-connected-412px-larger-android.png index 7d6182c5..a46c4bcf 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-34-responsive-leaderboard-connected-412px-larger-android.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-34-responsive-leaderboard-connected-412px-larger-android.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-after-disconnect.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-after-disconnect.png index c0b5e61c..dfda41ca 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-after-disconnect.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-after-disconnect.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-menu-open.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-menu-open.png index c0b5e61c..dfda41ca 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-menu-open.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-35-content-wallet-chip-menu-open.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-after-disconnect.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-after-disconnect.png index f1067cfb..4524cb2d 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-after-disconnect.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-after-disconnect.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-menu-open.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-menu-open.png index f1067cfb..4524cb2d 100644 Binary files a/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-menu-open.png and b/tests/widgets/superfluid-campaign-widget/test-results/scw-36-leaderboard-wallet-chip-menu-open.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-40-leaderboard-address-copied.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-40-leaderboard-address-copied.png new file mode 100644 index 00000000..ae46d025 Binary files /dev/null and b/tests/widgets/superfluid-campaign-widget/test-results/scw-40-leaderboard-address-copied.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-41-leaderboard-tooltip-hover.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-41-leaderboard-tooltip-hover.png new file mode 100644 index 00000000..1bdd7c13 Binary files /dev/null and b/tests/widgets/superfluid-campaign-widget/test-results/scw-41-leaderboard-tooltip-hover.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-42-ai-credits-cards.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-42-ai-credits-cards.png new file mode 100644 index 00000000..a28d7722 Binary files /dev/null and b/tests/widgets/superfluid-campaign-widget/test-results/scw-42-ai-credits-cards.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-43-standalone-links-content.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-43-standalone-links-content.png new file mode 100644 index 00000000..86fbeec6 Binary files /dev/null and b/tests/widgets/superfluid-campaign-widget/test-results/scw-43-standalone-links-content.png differ diff --git a/tests/widgets/superfluid-campaign-widget/test-results/scw-44-goodwallet-links-content.png b/tests/widgets/superfluid-campaign-widget/test-results/scw-44-goodwallet-links-content.png new file mode 100644 index 00000000..7e9b15a8 Binary files /dev/null and b/tests/widgets/superfluid-campaign-widget/test-results/scw-44-goodwallet-links-content.png differ