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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
GuidanceCardDefaultStory,
GuidanceCardHowToUseStory,
GuidanceCardFaqStory,
DownloadAntSeedStepStory,
} from '../helpers/aiCreditsWidgetStories'

const meta: Meta<typeof AiCreditsWidget> = {
Expand Down Expand Up @@ -160,7 +161,7 @@ export const GuidanceCardHowToUse: Story = {
const root = canvas.getByTestId('AiCreditsWidget-guidance-how-to-use')
const howToUseButton = within(root).getByRole('button', { name: /how to use/i })
await userEvent.click(howToUseButton)
await expect(within(root).getByText(/back to buying/i)).toBeVisible()
await expect(within(root).getByText(/Back to setup/i)).toBeVisible()
},
}

Expand All @@ -172,6 +173,11 @@ export const GuidanceCardFaq: Story = {
const root = canvas.getByTestId('AiCreditsWidget-guidance-faq')
const faqButton = within(root).getByRole('button', { name: /faqs/i })
await userEvent.click(faqButton)
await expect(within(root).getByText(/back to buying/i)).toBeVisible()
await expect(within(root).getByText(/Back to setup/i)).toBeVisible()
},
}

/** Setup tab — Download AntSeed step with active "Start ›" link and locked subsequent steps. */
export const DownloadAntSeedStep: Story = {
render: () => <DownloadAntSeedStepStory />,
}
16 changes: 16 additions & 0 deletions examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -602,3 +602,19 @@ export function GuidanceCardFaqStory() {
/>
)
}

/**
* Setup tab with wallet connected — shows Download AntSeed as the first
* actionable step with "Start ›" link, Signer Key and Authorize Wallet locked.
*/
export function DownloadAntSeedStepStory() {
return (
<MockStoryShell
dataTestId="AiCreditsWidget-download-antseed-step"
adapterFactory={createAdapterFactory('purchase_setup', {
gBalance: '54570',
activeTab: 'setup',
})}
/>
)
}
107 changes: 38 additions & 69 deletions packages/ai-credits-widget/src/AiCreditsWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import React, { useCallback, useMemo, useState } from 'react'
import { GoodWidgetProvider } from '@goodwidget/core'
import type { EIP1193Provider } from '@goodwidget/core'
import {
Badge,
BadgeText,
Button,
ButtonText,
Card,
CircularActionButton,
getChainDisplayName,
Heading,
Icon,
Text,
Expand All @@ -32,6 +35,7 @@ import {
SetupGuidanceCard,
HowToUseView,
SetupFaqView,
DownloadAntSeedStep,
} from './components'
import type {
AiCreditsWidgetProps,
Expand Down Expand Up @@ -87,23 +91,6 @@ function SetupConnectPrompt({
)
}

const SETUP_ONBOARDING_STEPS: Array<{ number: number; title: string; description: string }> = [
{
number: 1,
title: 'Download AntSeed',
description: 'Install the AntSeed application to manage your AI credits signer key.',
},
{
number: 2,
title: 'Signer Key',
description: 'Generate or import a signer key that AntSeed will use to authorize requests.',
},
{
number: 3,
title: 'Authorize Wallet',
description: 'Link your wallet to the signer key so it can be topped up with AI credits.',
},
]

function SetupTabPanel({
state,
Expand All @@ -123,34 +110,14 @@ function SetupTabPanel({

return (
<YStack gap="$4" width="100%">
<Heading level={5} secondary>
Onboarding
</Heading>
{SETUP_ONBOARDING_STEPS.map((step) => (
<Card key={step.number}>
<XStack gap="$3" alignItems="flex-start" padding="$3">
<YStack
width={28}
height={28}
borderRadius={14}
backgroundColor="$primary"
alignItems="center"
justifyContent="center"
flexShrink={0}
>
<Text fontWeight="700" fontSize="$2" color="$onPrimary">
{step.number}
</Text>
</YStack>
<YStack flex={1} gap="$1">
<Text fontWeight="700">{step.title}</Text>
<Text secondary fontSize="$2">
{step.description}
</Text>
</YStack>
</XStack>
</Card>
))}
<AiCreditsHero
gBalance={state.gBalance}
isGoodIdVerified={state.isGoodIdVerified}
/>
<Text secondary fontSize="$2">
One-time setup, in order — each step unlocks the next:
</Text>
<DownloadAntSeedStep />
</YStack>
)
}
Expand Down Expand Up @@ -448,8 +415,7 @@ function AiCreditsInner({

const handleTabChange = useCallback(
(tabId: string) => {
// Clear the help view when switching away from the buy tab.
if (tabId !== 'buy') {
if (tabId !== 'setup') {
setHelpView(null)
}
actions.setActiveTab(tabId as AiCreditsWidgetTab)
Expand All @@ -459,39 +425,35 @@ function AiCreditsInner({

const walletRequired = needsWalletConnection(state)

/** Activates a help view and ensures the buy tab is selected. */
const handleHelpViewOpen = useCallback(
(view: 'how-to-use' | 'faq') => {
actions.setActiveTab('buy')
actions.setActiveTab('setup')
setHelpView(view)
},
[actions],
)

/** Returns from the help view back to the normal buy content. */
const handleHelpViewClose = useCallback(() => {
setHelpView(null)
}, [])

/** Buy tab content: shows a help view when one is active, otherwise the purchase flow. */
const buyPanel = helpView === 'how-to-use' ? (
<HowToUseView onBack={handleHelpViewClose} />
) : helpView === 'faq' ? (
<SetupFaqView onBack={handleHelpViewClose} />
) : (
<BuyCreditsPanel
state={state}
actions={actions}
isPending={isPending}
onPay={(quote) => {
void handlePay(quote)
}}
/>
)
const setupPanel =
helpView === 'how-to-use' ? (
<HowToUseView onBack={handleHelpViewClose} />
) : helpView === 'faq' ? (
<SetupFaqView onBack={handleHelpViewClose} />
) : (
<SetupTabPanel state={state} onConnect={actions.connect} />
)

return (
<YStack gap="$3" padding="$3" width="100%">
{/* Guidance card is always rendered above the tab navigation when connected. */}
<XStack justifyContent="space-between" alignItems="center" paddingHorizontal="$1">
<Heading level={4}>GoodDollar</Heading>
<Badge type="info">
<BadgeText>{getChainDisplayName(state.chainId ?? CELO_CHAIN_ID)}</BadgeText>
</Badge>
</XStack>
<SetupGuidanceCard
activeHelpView={helpView}
onHowToUse={() => { handleHelpViewOpen('how-to-use') }}
Expand All @@ -507,11 +469,11 @@ function AiCreditsInner({
activeTab={walletRequired ? 'setup' : state.activeTab}
onTabChange={handleTabChange}
isTabDisabled={(tabId) => walletRequired && tabId !== 'setup'}
chainId={state.chainId ?? CELO_CHAIN_ID}
withConnectionStatus={false}
/>

{walletRequired || state.activeTab === 'setup' ? (
<SetupTabPanel state={state} onConnect={actions.connect} />
setupPanel
) : state.activeTab === 'manage' ? (
<ManagePanel state={state} actions={actions} />
) : state.activeTab === 'history' ? (
Expand All @@ -521,7 +483,14 @@ function AiCreditsInner({
knownBuyers={state.buyers.map((address) => ({ address }))}
/>
) : (
buyPanel
<BuyCreditsPanel
state={state}
actions={actions}
isPending={isPending}
onPay={(quote) => {
void handlePay(quote)
}}
/>
)}
</YStack>
)
Expand Down
1 change: 1 addition & 0 deletions packages/ai-credits-widget/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { HistoryTab } from './history/HistoryTab'
export { SetupGuidanceCard } from './setup/SetupGuidanceCard'
export { HowToUseView } from './setup/HowToUseView'
export { SetupFaqView } from './setup/SetupFaqView'
export { DownloadAntSeedStep } from './setup/DownloadAntSeedStep'
export type { AiCreditsFlowStep } from './flow/types'
export { getAiCreditsActiveFlowStep } from './flow/purchaseFlowUtils'
export { AiCreditsFlowStepper } from './flow/AiCreditsFlowStepper'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Stepper } from '@goodwidget/ui'
import type { StepperStepItem } from '@goodwidget/ui'

const ANTSEED_DOWNLOAD_URL = 'https://antseed.com'

const SETUP_STEPS: StepperStepItem[] = [
{
id: 'download',
title: 'Download Antseed',
description: 'The app that runs your credits locally — required once',
status: 'ready',
},
{
id: 'signer',
title: 'Signer key',
description: 'Generate or import — separate from your wallet',
status: 'pending',
},
{
id: 'authorize',
title: 'Authorize wallet',
description: 'One-time approval — scoped to credits only',
status: 'pending',
},
]

export function DownloadAntSeedStep() {
return (
<Stepper
steps={SETUP_STEPS}
activeStepId="download"
onStepPress={(stepId) => {
if (stepId !== 'download') return
if (typeof window === 'undefined') return
window.open(ANTSEED_DOWNLOAD_URL, '_blank', 'noopener,noreferrer')
}}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function HowToUseView({ onBack }: HowToUseViewProps) {
<Button variant="ghost" alignSelf="flex-start" onPress={onBack} gap="$1" paddingHorizontal="$0">
<Icon name="arrow-left" size="xs" color="primary" />
<ButtonText color="$primary" fontSize="$2">
Back to buying
Back to setup
</ButtonText>
</Button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function SetupFaqView({ onBack }: SetupFaqViewProps) {
<Button variant="ghost" alignSelf="flex-start" onPress={onBack} gap="$1" paddingHorizontal="$0">
<Icon name="arrow-left" size="xs" color="primary" />
<ButtonText color="$primary" fontSize="$2">
Back to buying
Back to setup
</ButtonText>
</Button>

Expand Down
Loading
Loading