Skip to content
Open
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 @@ -45,7 +45,8 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/
<DocsCard title="S3 – Quote Ready">Buy tab — pay step available via drawer CTA; ready to confirm Celo transaction.</DocsCard>
<DocsCard title="S4 – Payment Pending">Buy tab — Celo tx submitted.</DocsCard>
<DocsCard title="S5 – Payment Confirmed">Buy tab — settling on Base.</DocsCard>
<DocsCard title="S6 – Manage tab">Manage tab — balance, stream, history, close/withdraw, API setup.</DocsCard>
<DocsCard title="S6 – Manage tab">Manage tab — balance, stream, close/withdraw, API setup.</DocsCard>
<DocsCard title="S6b – History tab">History tab — filtered, paginated AI credit funding history.</DocsCard>
<DocsCard title="S7 – Insufficient G$">G$ below minimum purchase threshold.</DocsCard>
<DocsCard title="S8 – Payment Failed">Transaction or settlement error.</DocsCard>
<DocsCard title="S9 – Backend Unavailable">Backend unreachable.</DocsCard>
Expand All @@ -55,11 +56,12 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/

<DocsSection
title="Widget navigation"
description="When a wallet is connected, Buy Credits and Manage appear as tabs at the top (with the GoodDollar header and chain badge). Switch tabs anytime — including during payment pending or failed states."
description="When a wallet is connected, Buy Credits, Manage, and History appear as tabs at the top (with the GoodDollar header and chain badge). Switch tabs anytime — including during payment pending or failed states."
>
<DocsGrid>
<DocsCard title="Buy Credits">Stepper for buyer key, consent, and pay — tap a step or CTA to open it in a drawer.</DocsCard>
<DocsCard title="Manage">Credits balance, stream info, close/withdraw, API setup, and history.</DocsCard>
<DocsCard title="Manage">Credits balance, stream info, close/withdraw, and API setup.</DocsCard>
<DocsCard title="History">Paginated credit history with source, status, and date filters.</DocsCard>
</DocsGrid>
</DocsSection>

Expand All @@ -68,7 +70,10 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/
description="Production stories call the antseed-integration worker for reads, settlement, and management actions."
>
<DocsGrid>
<DocsCard title="GET …/credit">Credits profile and funding history for the payer wallet.</DocsCard>
<DocsCard title="GET …/profile">Credits profile for the payer wallet.</DocsCard>
<DocsCard title="GET …/credit-history">
Newest-first funding history (`items[].buyerAddress` is used to recover the buyer).
</DocsCard>
<DocsCard title="GET …/outstanding">Pending or failed Base funding entries.</DocsCard>
<DocsCard title="POST …/celo/events/record">Notify `{ txHash }` after a successful Celo payment.</DocsCard>
<DocsCard title="POST …/accounts/:buyer/operator-consent">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PaymentPendingStory,
PaymentConfirmedStory,
CreditsManagementStory,
HistoryTabStory,
InsufficientGBalanceStory,
PaymentFailedStory,
BackendUnavailableStory,
Expand Down Expand Up @@ -63,6 +64,10 @@ export const CreditsManagement: Story = {
render: () => <CreditsManagementStory />,
}

export const HistoryTab: Story = {
render: () => <HistoryTabStory />,
}

export const InsufficientGBalance: Story = {
render: () => <InsufficientGBalanceStory />,
}
Expand Down
18 changes: 18 additions & 0 deletions examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,24 @@ export function ManageTabStory() {
)
}

export function HistoryTabStory() {
return (
<MockStoryShell
dataTestId="AiCreditsWidget-history-tab"
adapterFactory={createAdapterFactory('quote_ready', {
totalCreditUsd: '110000000',
buyerPubKey: '0xfc128652c9b397a1f89A9EC84E798B869B0E4c7a',
operatorConsented: true,
operatorAddress: '0x0000000000000000000000000000000000000004',
totalGdDepositedG: '50.00',
monthlyStreamG: '5.00',
gBalance: '42.50',
activeTab: 'history',
})}
/>
)
}

export function CreditsManagementStory() {
return <ManageTabStory />
}
Expand Down
23 changes: 11 additions & 12 deletions packages/ai-credits-widget/src/AiCreditsWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
updateToast,
} from '@goodwidget/ui'
import { useAiCreditsAdapter } from './adapter'
import { useAiCreditsHistory } from './useAiCreditsHistory'
import { DEPOSIT_BONUS_PERCENT, STREAM_BONUS_PERCENT } from './quoteMath'
import {
AiCreditsHero,
Expand All @@ -27,7 +28,7 @@ import {
CreditsManagementCard,
BuyerOperatorCard,
SetupSnippet,
UsageLog,
HistoryTab,
} from './components'
import type {
AiCreditsWidgetProps,
Expand Down Expand Up @@ -233,20 +234,16 @@ function BuyCreditsPanel({
function ManagePanel({
state,
actions,
backendUrl,
}: {
state: AiCreditsWidgetAdapterState
actions: AiCreditsWidgetAdapterActions
backendUrl?: string
}) {
const [refreshing, setRefreshing] = React.useState(false)
const [usageLogRefreshSignal, setUsageLogRefreshSignal] = React.useState(0)

const handleRefresh = async () => {
setRefreshing(true)
try {
await actions.refresh()
setUsageLogRefreshSignal((value) => value + 1)
} finally {
setRefreshing(false)
}
Expand All @@ -268,12 +265,6 @@ function ManagePanel({

<SetupSnippet />

<UsageLog
address={state.address}
backendUrl={backendUrl}
refreshSignal={usageLogRefreshSignal}
/>

<YStack gap="$2" width="100%" alignItems="center">
{state.error && (
<Text color="$error" fontSize="$2" textAlign="center">
Expand Down Expand Up @@ -337,6 +328,11 @@ function AiCreditsInner({

const { state, actions } = activeAdapter

const history = useAiCreditsHistory({
address: state.address,
backendUrl,
})

const handlePay = useCallback(
async (quote: AiCreditsQuote) => {
const toastId = createToast({
Expand Down Expand Up @@ -401,13 +397,16 @@ function AiCreditsInner({
tabs={[
{ id: 'buy', label: 'Buy Credits' },
{ id: 'manage', label: 'Manage' },
{ id: 'history', label: 'History' },
]}
activeTab={state.activeTab}
onTabChange={handleTabChange}
chainId={state.chainId ?? CELO_CHAIN_ID}
/>
{state.activeTab === 'manage' ? (
<ManagePanel state={state} actions={actions} backendUrl={backendUrl} />
<ManagePanel state={state} actions={actions} />
) : state.activeTab === 'history' ? (
<HistoryTab state={history.state} actions={history.actions} />
) : (
buyPanel
)}
Expand Down
40 changes: 23 additions & 17 deletions packages/ai-credits-widget/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ function isInBuyFlowStatus(status: AiCreditsWidgetStatus): boolean {
)
}

function isNonBuyTab(tab: AiCreditsWidgetTab): boolean {
return tab === 'manage' || tab === 'history'
}

function resolveActiveTab(
prev: AiCreditsWidgetAdapterState,
overrides: Partial<AiCreditsWidgetAdapterState>,
Expand Down Expand Up @@ -182,7 +186,7 @@ function deriveStatus(params: {

if (chainId !== null && chainId !== CELO_CHAIN_ID) return 'unsupported_chain'

if (error && activeTab !== 'manage') return 'payment_failed'
if (error && !isNonBuyTab(activeTab)) return 'payment_failed'

if (gBalance === null) return 'purchase_setup'

Expand Down Expand Up @@ -224,13 +228,15 @@ function withDerivedStatus(
}
}

function mergeStatePreservingManageTab(
function mergeStatePreservingNonBuyTab(
prev: AiCreditsWidgetAdapterState,
overrides: Partial<AiCreditsWidgetAdapterState>,
): AiCreditsWidgetAdapterState {
if (prev.activeTab !== 'manage') {
const nextTab = overrides.activeTab ?? prev.activeTab
if (!isNonBuyTab(prev.activeTab) && !isNonBuyTab(nextTab)) {
return withDerivedStatus(prev, overrides, true)
}
const activeTab = isNonBuyTab(nextTab) ? nextTab : prev.activeTab
const status = deriveStatus({
isConnected: true,
chainId: overrides.chainId ?? prev.chainId,
Expand All @@ -240,13 +246,13 @@ function mergeStatePreservingManageTab(
operatorConsented: overrides.operatorConsented ?? prev.operatorConsented,
error: overrides.error ?? prev.error,
currentStatus: overrides.status ?? prev.status,
activeTab: 'manage',
activeTab,
})
return {
...prev,
...overrides,
status,
activeTab: 'manage',
activeTab,
}
}

Expand Down Expand Up @@ -570,11 +576,11 @@ export function useAiCreditsAdapter({
})

setState((prev) =>
mergeStatePreservingManageTab(prev, {
mergeStatePreservingNonBuyTab(prev, {
buyerPubKey: account.address,
buyerPrvKey: privateKey,
error: null,
...(prev.activeTab !== 'manage' ? { status: 'purchase_setup' } : {}),
...(!isNonBuyTab(prev.activeTab) ? { status: 'purchase_setup' } : {}),
}),
)
} catch (err: unknown) {
Expand All @@ -596,7 +602,7 @@ export function useAiCreditsAdapter({
}

const ref: AccountRef = { payer: currentState.address, buyer: currentState.buyerPubKey }
const onManageTab = currentState.activeTab === 'manage'
const onNonBuyTab = isNonBuyTab(currentState.activeTab)

try {
const operatorStatus = await chainClient.getBuyerOperatorStatus(ref)
Expand All @@ -608,10 +614,10 @@ export function useAiCreditsAdapter({
if (operatorStatus.operatorAccepted) {
patchPayerSession(currentState.address, { operatorConsented: true })
setState((prev) =>
mergeStatePreservingManageTab(prev, {
mergeStatePreservingNonBuyTab(prev, {
operatorConsented: true,
error: null,
...(!onManageTab ? { status: 'purchase_setup' } : {}),
...(!onNonBuyTab ? { status: 'purchase_setup' } : {}),
}),
)
return
Expand All @@ -636,10 +642,10 @@ export function useAiCreditsAdapter({

patchPayerSession(currentState.address, { operatorConsented: true })
setState((prev) =>
mergeStatePreservingManageTab(prev, {
mergeStatePreservingNonBuyTab(prev, {
operatorConsented: true,
error: null,
...(!onManageTab ? { status: 'purchase_setup' } : {}),
...(!onNonBuyTab ? { status: 'purchase_setup' } : {}),
}),
)
} catch (err: unknown) {
Expand All @@ -662,12 +668,12 @@ export function useAiCreditsAdapter({
if (!operatorStatus.operatorAccepted) return

patchPayerSession(currentState.address, { operatorConsented: true })
const onManageTab = currentState.activeTab === 'manage'
const onNonBuyTab = isNonBuyTab(currentState.activeTab)
setState((prev) =>
mergeStatePreservingManageTab(prev, {
mergeStatePreservingNonBuyTab(prev, {
operatorConsented: true,
error: null,
...(!onManageTab ? { status: 'purchase_setup' } : {}),
...(!onNonBuyTab ? { status: 'purchase_setup' } : {}),
}),
)
} catch {
Expand Down Expand Up @@ -898,7 +904,7 @@ export function useAiCreditsAdapter({
})
} catch {
setState((prev) =>
mergeStatePreservingManageTab(prev, {
mergeStatePreservingNonBuyTab(prev, {
status: 'backend_unavailable',
error: 'Could not reach backend — check your connection',
}),
Expand Down Expand Up @@ -1091,7 +1097,7 @@ export function useAiCreditsAdapter({
)
return
}
setState((prev) => mergeStatePreservingManageTab(prev, { activeTab: 'manage', error: null }))
setState((prev) => mergeStatePreservingNonBuyTab(prev, { activeTab: tab, error: null }))
}, [])

const handleStartPurchase = useCallback(() => {
Expand Down
Loading