diff --git a/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidget.mdx b/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidget.mdx
index e21fd4b4..b3137e34 100644
--- a/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidget.mdx
+++ b/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidget.mdx
@@ -45,7 +45,8 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/
Buy tab — pay step available via drawer CTA; ready to confirm Celo transaction.
Buy tab — Celo tx submitted.
Buy tab — settling on Base.
- Manage tab — balance, stream, history, close/withdraw, API setup.
+ Manage tab — balance, stream, close/withdraw, API setup.
+ History tab — filtered, paginated AI credit funding history.
G$ below minimum purchase threshold.
Transaction or settlement error.
Backend unreachable.
@@ -55,11 +56,12 @@ import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/
Stepper for buyer key, consent, and pay — tap a step or CTA to open it in a drawer.
- Credits balance, stream info, close/withdraw, API setup, and history.
+ Credits balance, stream info, close/withdraw, and API setup.
+ Paginated credit history with source, status, and date filters.
diff --git a/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidgetQA.stories.tsx b/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidgetQA.stories.tsx
index edf8a8f1..c562c31b 100644
--- a/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidgetQA.stories.tsx
+++ b/examples/storybook/src/stories/ai-credits-widget/AiCreditsWidgetQA.stories.tsx
@@ -10,6 +10,7 @@ import {
PaymentPendingStory,
PaymentConfirmedStory,
CreditsManagementStory,
+ HistoryTabStory,
InsufficientGBalanceStory,
PaymentFailedStory,
BackendUnavailableStory,
@@ -65,6 +66,10 @@ export const CreditsManagement: Story = {
render: () => ,
}
+export const HistoryTab: Story = {
+ render: () => ,
+}
+
export const InsufficientGBalance: Story = {
render: () => ,
}
diff --git a/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx b/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
index c15c1d83..de260e3a 100644
--- a/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
+++ b/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
@@ -210,6 +210,24 @@ export function ManageTabStory() {
)
}
+export function HistoryTabStory() {
+ return (
+
+ )
+}
+
export function CreditsManagementStory() {
return
}
diff --git a/packages/ai-credits-widget/src/AiCreditsWidget.tsx b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
index af0e2042..81c75c2e 100644
--- a/packages/ai-credits-widget/src/AiCreditsWidget.tsx
+++ b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
@@ -17,6 +17,8 @@ import {
updateToast,
} from '@goodwidget/ui'
import { useAiCreditsAdapter } from './adapter'
+import { useAiCreditsHistory } from './useAiCreditsHistory'
+import { DEPOSIT_BONUS_PERCENT, STREAM_BONUS_PERCENT } from './quoteMath'
import {
AiCreditsHero,
AiCreditsFlowStepper,
@@ -26,7 +28,7 @@ import {
CreditsManagementCard,
BuyerOperatorCard,
SetupSnippet,
- UsageLog,
+ HistoryTab,
} from './components'
import type {
AiCreditsWidgetProps,
@@ -216,20 +218,16 @@ function BuyCreditsPanel({ state, actions, isPending, onPay }: BuyPanelProps) {
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)
}
@@ -251,12 +249,6 @@ function ManagePanel({
-
-
{state.error && (
@@ -313,6 +305,11 @@ function AiCreditsInner({
const { state, actions } = activeAdapter
+ const history = useAiCreditsHistory({
+ address: state.address,
+ backendUrl,
+ })
+
const handlePay = useCallback(
async (quote: AiCreditsQuote) => {
const toastId = createToast({
@@ -373,13 +370,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' ? (
-
+
+ ) : state.activeTab === 'history' ? (
+
) : (
buyPanel
)}
diff --git a/packages/ai-credits-widget/src/adapter.ts b/packages/ai-credits-widget/src/adapter.ts
index 0cbc64d7..0c2283f0 100644
--- a/packages/ai-credits-widget/src/adapter.ts
+++ b/packages/ai-credits-widget/src/adapter.ts
@@ -110,6 +110,20 @@ const WALLET_LOADING_STATE: Partial = {
operatorAddress: null,
}
+function isInBuyFlowStatus(status: AiCreditsWidgetStatus): boolean {
+ return (
+ status === 'purchase_setup' ||
+ status === 'quote_ready' ||
+ status === 'payment_pending' ||
+ status === 'payment_confirmed' ||
+ status === 'payment_failed'
+ )
+}
+
+function isNonBuyTab(tab: AiCreditsWidgetTab): boolean {
+ return tab === 'manage' || tab === 'history'
+}
+
function resolveActiveTab(
prev: AiCreditsWidgetAdapterState,
overrides: Partial,
@@ -169,7 +183,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'
@@ -211,13 +225,15 @@ function withDerivedStatus(
}
}
-function mergeStatePreservingManageTab(
+function mergeStatePreservingNonBuyTab(
prev: AiCreditsWidgetAdapterState,
overrides: Partial,
): 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,
@@ -227,13 +243,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,
}
}
@@ -558,11 +574,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) {
@@ -592,7 +608,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)
@@ -604,10 +620,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
@@ -632,10 +648,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) {
@@ -658,12 +674,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 {
@@ -902,7 +918,7 @@ export function useAiCreditsAdapter({
})
} catch {
setState((prev) =>
- mergeStatePreservingManageTab(prev, {
+ mergeStatePreservingNonBuyTab(prev, {
status: 'backend_unavailable',
error: 'Could not reach backend — check your connection',
}),
@@ -1097,7 +1113,7 @@ export function useAiCreditsAdapter({
)
return
}
- setState((prev) => mergeStatePreservingManageTab(prev, { activeTab: 'manage', error: null }))
+ setState((prev) => mergeStatePreservingNonBuyTab(prev, { activeTab: tab, error: null }))
}, [])
const handleStartPurchase = useCallback(() => {
diff --git a/packages/ai-credits-widget/src/backendClient.ts b/packages/ai-credits-widget/src/backendClient.ts
index 8bd5c28d..7d7c0871 100644
--- a/packages/ai-credits-widget/src/backendClient.ts
+++ b/packages/ai-credits-widget/src/backendClient.ts
@@ -163,6 +163,74 @@ function paginateGdCredits(
}
}
+function createDemoHistory(account: string): GdCreditEntry[] {
+ const normalized = normalizeAddress(account)
+ return [
+ {
+ id: 'demo-deposit-1',
+ account: normalized,
+ rootAccount: normalized,
+ source: 'deposit',
+ gdAmountWei: '440000000000000000000',
+ totalCreditUsd: '6600',
+ principalUsd: '6000',
+ bonusUsd: '600',
+ fundingStatus: 'funded',
+ txHash: '0xdemo01',
+ logIndex: 0,
+ createdAt: '2026-06-25T14:30:00.000Z',
+ streamUpdateMonth: '2026-06',
+ buyerAddress: normalized,
+ },
+ {
+ id: 'demo-stream-update-1',
+ account: normalized,
+ rootAccount: normalized,
+ source: 'streamUpdate',
+ gdAmountWei: '10000000000000000000',
+ totalCreditUsd: '0',
+ principalUsd: '0',
+ bonusUsd: '0',
+ fundingStatus: 'funded',
+ txHash: '0xdemo02',
+ logIndex: 0,
+ createdAt: '2026-06-24T12:00:00.000Z',
+ streamUpdateMonth: '2026-06',
+ buyerAddress: normalized,
+ },
+ {
+ id: 'demo-stream-request-1',
+ account: normalized,
+ rootAccount: normalized,
+ source: 'streamRequest',
+ gdAmountWei: '8000000000000000000',
+ totalCreditUsd: '1200',
+ principalUsd: '1000',
+ bonusUsd: '200',
+ fundingStatus: 'pending',
+ txHash: '0xdemo03',
+ logIndex: 0,
+ createdAt: '2026-06-23T09:15:00.000Z',
+ streamUpdateMonth: '2026-06',
+ buyerAddress: normalized,
+ },
+ {
+ id: 'demo-stream-cron-1',
+ account: normalized,
+ rootAccount: normalized,
+ source: 'streamCron',
+ gdAmountWei: '5000000000000000000',
+ totalCreditUsd: '900',
+ principalUsd: '750',
+ bonusUsd: '150',
+ fundingStatus: 'funded',
+ createdAt: '2026-06-22T08:00:00.000Z',
+ streamUpdateMonth: '2026-06',
+ buyerAddress: normalized,
+ },
+ ]
+}
+
export function resolveBuyerAddress(entries: GdCreditEntry[]): string | null {
for (const entry of entries) {
if (entry.buyerAddress && isAddress(entry.buyerAddress)) {
@@ -234,7 +302,6 @@ export interface AiCreditsBackendClient {
payer: string,
options?: { status?: 'pending' | 'funded' | 'failed'; limit?: number; cursor?: string },
): Promise
- getUsageLog(payer: string): Promise
notifyPayment(txHash: string): Promise
waitForSettlement(
ref: AccountRef,
@@ -304,7 +371,7 @@ export class MockAiCreditsBackendClient implements AiCreditsBackendClient {
this.accountStates.set(key, {
principalUsd: 0n,
bonusUsd: 0n,
- transactions: [],
+ transactions: createDemoHistory(key),
rootAccount: key,
})
}
diff --git a/packages/ai-credits-widget/src/components/history/HistoryTab.tsx b/packages/ai-credits-widget/src/components/history/HistoryTab.tsx
new file mode 100644
index 00000000..61b069d9
--- /dev/null
+++ b/packages/ai-credits-widget/src/components/history/HistoryTab.tsx
@@ -0,0 +1,621 @@
+import React, { useMemo, useState } from 'react'
+import {
+ Button,
+ ButtonText,
+ Card,
+ Heading,
+ Icon,
+ Input,
+ Spinner,
+ Text,
+ XStack,
+ YStack,
+} from '@goodwidget/ui'
+import type { IconName } from '@goodwidget/ui'
+import type { GdCreditEntry } from '../../backendTypes'
+import { usdToCredits, weiToG } from '../../quoteMath'
+import { compactButtonProps } from '../shared/styles'
+import type {
+ AiCreditsHistoryActions,
+ AiCreditsHistoryState,
+ CreditHistorySource,
+ CreditHistoryStatusFilter,
+} from '../../useAiCreditsHistory'
+import {
+ HISTORY_LOOKBACK_DAYS,
+ getLast90DaysRange,
+} from '../../useAiCreditsHistory'
+
+const STATUS_OPTIONS = [
+ { label: 'All', value: 'all' },
+ { label: 'Funded', value: 'funded' },
+ { label: 'Pending', value: 'pending' },
+ { label: 'Failed', value: 'failed' },
+]
+
+const SOURCE_PILL_OPTIONS: { id: CreditHistorySource; label: string }[] = [
+ { id: 'deposit', label: 'Deposit' },
+ { id: 'streamUpdate', label: 'Stream update' },
+ { id: 'streamRequest', label: 'Stream credit' },
+]
+
+export interface HistoryTabProps {
+ state: AiCreditsHistoryState
+ actions: AiCreditsHistoryActions
+}
+
+function sourceLabel(source: CreditHistorySource): string {
+ if (source === 'deposit') return 'G$ Deposit'
+ if (source === 'streamUpdate') return 'Stream Update'
+ return 'Stream Credit'
+}
+
+function formatEntryDate(createdAt: string): string {
+ const date = new Date(createdAt)
+ if (Number.isNaN(date.getTime())) return createdAt
+ return date.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })
+}
+
+function formatCredits(totalCreditUsd: string): string | null {
+ const micro = BigInt(totalCreditUsd || '0')
+ if (micro <= 0n) return null
+ return `+${usdToCredits(micro.toString())} cr`
+}
+
+function formatGAmount(gdAmountWei: string): string | null {
+ const amountWei = BigInt(gdAmountWei || '0')
+ if (amountWei <= 0n) return null
+ return `${weiToG(amountWei)} G$`
+}
+
+function amountSummary(entry: GdCreditEntry): string | null {
+ if (entry.source === 'streamUpdate') {
+ const gAmount = formatGAmount(entry.gdAmountWei)
+ return gAmount ? `${gAmount}/mo` : null
+ }
+ const credits = formatCredits(entry.totalCreditUsd)
+ if (credits) return credits
+ if (entry.fundingStatus === 'failed') {
+ return `${usdToCredits(entry.totalCreditUsd || '0')} cr`
+ }
+ return formatGAmount(entry.gdAmountWei)
+}
+
+function entryDetailRows(entry: GdCreditEntry): { label: string; value: string }[] {
+ const rows: { label: string; value: string }[] = [
+ { label: 'Type', value: sourceLabel(entry.source) },
+ { label: 'Status', value: statusLabel(entry.fundingStatus) },
+ { label: 'Credits', value: `${usdToCredits(entry.totalCreditUsd)} cr` },
+ ]
+ const gAmount = formatGAmount(entry.gdAmountWei)
+ if (gAmount) rows.push({ label: 'G$', value: gAmount })
+ if (entry.source !== 'streamUpdate') {
+ rows.push(
+ { label: 'Principal', value: `${usdToCredits(entry.principalUsd)} cr` },
+ { label: 'Bonus', value: `${usdToCredits(entry.bonusUsd)} cr` },
+ )
+ }
+ if (entry.txHash) rows.push({ label: 'Celo tx', value: entry.txHash })
+ if (entry.fundingTxHash) rows.push({ label: 'Funding tx', value: entry.fundingTxHash })
+ return rows
+}
+
+function EntryDetailRow({ label, value }: { label: string; value: string }) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ )
+}
+
+function entryAccent(entry: GdCreditEntry): string {
+ if (entry.fundingStatus === 'failed') return '$error'
+ if (entry.fundingStatus === 'pending') return '$warning'
+ return '$success'
+}
+
+function amountAccent(entry: GdCreditEntry): string {
+ if (entry.fundingStatus === 'failed') return '$error'
+ if (entry.source === 'streamUpdate') return '$color'
+ if (entry.fundingStatus === 'pending') return '$warning'
+ return '$success'
+}
+
+function entryIconName(entry: GdCreditEntry): IconName {
+ if (entry.fundingStatus === 'failed') return 'alert-circle'
+ if (entry.source === 'deposit') return 'plus'
+ if (entry.source === 'streamUpdate') return 'refresh'
+ return 'arrow-down'
+}
+
+function entryIconColor(entry: GdCreditEntry): 'error' | 'warning' | 'primary' | 'success' {
+ if (entry.fundingStatus === 'failed') return 'error'
+ if (entry.fundingStatus === 'pending') return 'warning'
+ if (entry.source === 'streamUpdate') return 'primary'
+ return 'success'
+}
+
+function entryIconBackground(entry: GdCreditEntry): string {
+ if (entry.fundingStatus === 'failed') return '$errorMuted'
+ if (entry.fundingStatus === 'pending') return '$warningMuted'
+ if (entry.source === 'streamUpdate') return '$infoMuted'
+ return '$successMuted'
+}
+
+function statusLabel(status: GdCreditEntry['fundingStatus']): string {
+ return status.toUpperCase()
+}
+
+function sumFilteredGdWei(entries: GdCreditEntry[]): bigint {
+ return entries.reduce((sum, entry) => {
+ if (entry.source === 'streamUpdate') return sum
+ try {
+ const amount = BigInt(entry.gdAmountWei || '0')
+ return amount > 0n ? sum + amount : sum
+ } catch {
+ return sum
+ }
+ }, 0n)
+}
+
+function formatAccumulatedG(amountWei: bigint): string {
+ const value = Number(amountWei) / 1e18
+ if (!Number.isFinite(value) || value < 0) return '0.00 G$'
+ return `${new Intl.NumberFormat('en-US', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(value)} G$`
+}
+
+function formatShortDate(dateValue: string): string {
+ const date = new Date(`${dateValue}T00:00:00.000Z`)
+ if (Number.isNaN(date.getTime())) return dateValue
+ return date.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ timeZone: 'UTC',
+ })
+}
+
+function escapeCsvValue(value: string): string {
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
+ return `"${value.replace(/"/g, '""')}"`
+ }
+ return value
+}
+
+function exportEntriesCsv(entries: GdCreditEntry[]): void {
+ if (typeof document === 'undefined') return
+ const headers = [
+ 'id',
+ 'source',
+ 'status',
+ 'credits',
+ 'gdAmountWei',
+ 'createdAt',
+ 'txHash',
+ 'fundingTxHash',
+ ]
+ const rows = entries.map((entry) =>
+ [
+ entry.id,
+ entry.source,
+ entry.fundingStatus,
+ usdToCredits(entry.totalCreditUsd),
+ entry.gdAmountWei,
+ entry.createdAt,
+ entry.txHash ?? '',
+ entry.fundingTxHash ?? '',
+ ]
+ .map((value) => escapeCsvValue(String(value)))
+ .join(','),
+ )
+ const csv = [headers.join(','), ...rows].join('\n')
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
+ const url = URL.createObjectURL(blob)
+ const anchor = document.createElement('a')
+ anchor.href = url
+ anchor.download = 'ai-credit-history.csv'
+ anchor.click()
+ URL.revokeObjectURL(url)
+}
+
+function CreditHistoryEntryRow({
+ entry,
+ expanded,
+ onToggle,
+}: {
+ entry: GdCreditEntry
+ expanded: boolean
+ onToggle: () => void
+}) {
+ const accent = entryAccent(entry)
+ const amountColor = amountAccent(entry)
+ const amount = amountSummary(entry)
+ const titleColor = entry.fundingStatus === 'failed' ? '$error' : '$color'
+
+ return (
+
+
+
+
+
+
+
+
+ {sourceLabel(entry.source)}
+
+
+
+
+ {statusLabel(entry.fundingStatus)}
+
+
+
+
+
+ {amount ? (
+
+ {amount}
+
+ ) : null}
+
+ {formatEntryDate(entry.createdAt)}
+
+
+
+
+ {expanded ? (
+
+ {entryDetailRows(entry).map((row) => (
+
+ ))}
+
+ ) : null}
+
+ )
+}
+
+function SourceFilterChip({
+ label,
+ selected,
+ onPress,
+}: {
+ label: string
+ selected: boolean
+ onPress: () => void
+}) {
+ return (
+
+
+ {label}
+
+
+ )
+}
+
+function StatusFilterSelect({
+ value,
+ onValueChange,
+}: {
+ value: CreditHistoryStatusFilter
+ onValueChange: (value: CreditHistoryStatusFilter) => void
+}) {
+ const [open, setOpen] = useState(false)
+ const selected = STATUS_OPTIONS.find((option) => option.value === value)
+
+ return (
+
+ setOpen((current) => !current)}
+ >
+
+ Status: {selected?.label ?? 'All'}
+
+
+
+
+ {open ? (
+
+ {STATUS_OPTIONS.map((option) => (
+ {
+ onValueChange(option.value as CreditHistoryStatusFilter)
+ setOpen(false)
+ }}
+ >
+
+ {option.label}
+
+
+ ))}
+
+ ) : null}
+
+ )
+}
+
+export function HistoryTab({ state, actions }: HistoryTabProps) {
+ const {
+ selectedSources,
+ statusFilter,
+ fromDate,
+ toDate,
+ entries,
+ hasMore,
+ loading,
+ loadingMore,
+ error,
+ activeSources,
+ } = state
+ const [expandedEntryId, setExpandedEntryId] = useState(null)
+ const defaultRange = useMemo(() => getLast90DaysRange(), [])
+ const isDefaultRange = fromDate === defaultRange.from && toDate === defaultRange.to
+ const rangeSummary = isDefaultRange
+ ? `Last ${HISTORY_LOOKBACK_DAYS} days activity`
+ : `${formatShortDate(fromDate)} – ${formatShortDate(toDate)}`
+ const accumulatedG = useMemo(
+ () => formatAccumulatedG(sumFilteredGdWei(entries)),
+ [entries],
+ )
+
+ return (
+
+
+
+
+ AI Credit History
+
+ {rangeSummary}
+
+
+
+
+
+
+ TOTAL ACCUMULATED
+
+
+ {accumulatedG}
+
+
+
+
+
+ {SOURCE_PILL_OPTIONS.map((option) => (
+
+ actions.setSourceChecked(option.id, !selectedSources[option.id])
+ }
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CREDIT History
+
+ 0 ? 'pointer' : 'default'}
+ opacity={entries.length > 0 ? 1 : 0.5}
+ hoverStyle={{ opacity: entries.length > 0 ? 0.85 : 0.5 }}
+ pressStyle={{ opacity: entries.length > 0 ? 0.7 : 0.5 }}
+ onPress={() => {
+ if (entries.length === 0) return
+ exportEntriesCsv(entries)
+ }}
+ >
+
+ Export CSV
+
+
+
+
+ {loading ? (
+
+
+
+
+ Loading credit history…
+
+
+
+ ) : error ? (
+
+
+
+ {error}
+
+
+
+
+ ) : activeSources.length === 0 ? (
+
+
+ Select at least one source to view history.
+
+
+ ) : entries.length === 0 ? (
+
+
+ No credit history matches these filters.
+
+
+ ) : (
+
+ {entries.map((entry) => (
+
+ setExpandedEntryId((current) => (current === entry.id ? null : entry.id))
+ }
+ />
+ ))}
+
+ )}
+
+ {!loading && !error && hasMore && (
+
+ )}
+
+ )
+}
diff --git a/packages/ai-credits-widget/src/components/index.ts b/packages/ai-credits-widget/src/components/index.ts
index 1b66eb87..5ece20ed 100644
--- a/packages/ai-credits-widget/src/components/index.ts
+++ b/packages/ai-credits-widget/src/components/index.ts
@@ -7,7 +7,7 @@ export { BuyCreditsFaq } from './buy/BuyCreditsFaq'
export { CreditsManagementCard } from './manage/CreditsManagementCard'
export { BuyerOperatorCard } from './manage/BuyerOperatorCard'
export { SetupSnippet } from './manage/SetupSnippet'
-export { UsageLog } from './manage/UsageLog'
+export { HistoryTab } from './history/HistoryTab'
export type { AiCreditsFlowStep } from './flow/types'
export { getAiCreditsActiveFlowStep } from './flow/purchaseFlowUtils'
export { AiCreditsFlowStepper } from './flow/AiCreditsFlowStepper'
diff --git a/packages/ai-credits-widget/src/components/manage/UsageLog.tsx b/packages/ai-credits-widget/src/components/manage/UsageLog.tsx
deleted file mode 100644
index 8cd8e420..00000000
--- a/packages/ai-credits-widget/src/components/manage/UsageLog.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React, { useEffect, useMemo, useState } from 'react'
-import { Card, Icon, Separator, Spinner, Text, XStack, YStack } from '@goodwidget/ui'
-import type { GdCreditEntry } from '../../backendTypes'
-import { createBackendClient } from '../../backendClient'
-import { formatUsdMicroDisplay, weiToG } from '../../quoteMath'
-
-const INITIAL_VISIBLE_ENTRIES = 4
-
-interface UsageLogProps {
- address: string | null
- backendUrl?: string
- refreshSignal?: number
-}
-
-function sourceLabel(source: GdCreditEntry['source']): string {
- if (source === 'deposit') return 'G$ deposit'
- return 'G$ stream'
-}
-
-function entryLabel(entry: GdCreditEntry): string {
- if (entry.fundingStatus === 'failed') {
- return `${sourceLabel(entry.source)} (failed)`
- }
- return sourceLabel(entry.source)
-}
-
-function formatGAmountWei(gdAmountWei: string): string | null {
- const amountWei = BigInt(gdAmountWei || '0')
- if (amountWei <= 0n) return null
- return `${weiToG(amountWei)} G$`
-}
-
-function formatFundedSummary(entries: GdCreditEntry[]): string {
- let usdMicroTotal = 0n
- let gdWeiTotal = 0n
-
- for (const entry of entries) {
- if (entry.fundingStatus !== 'funded') continue
- usdMicroTotal += BigInt(entry.totalCreditUsd || '0')
- gdWeiTotal += BigInt(entry.gdAmountWei || '0')
- }
-
- const parts: string[] = []
- const gTotal = formatGAmountWei(gdWeiTotal.toString())
- if (gTotal) parts.push(gTotal)
- parts.push(
- usdMicroTotal > 0n ? formatUsdMicroDisplay(usdMicroTotal.toString()) : formatUsdMicroDisplay('0'),
- )
- return parts.join(' · ')
-}
-
-function CreditHistoryEntry({ entry }: { entry: GdCreditEntry }) {
- const failed = entry.fundingStatus === 'failed'
- const amountColor = failed ? '$error' : '$color'
- const gAmountDisplay = formatGAmountWei(entry.gdAmountWei)
-
- return (
-
-
-
-
-
- {entryLabel(entry)}
-
-
- {new Date(entry.createdAt).toLocaleString()}
-
-
-
- {gAmountDisplay && (
-
- +{gAmountDisplay}
-
- )}
-
- +{formatUsdMicroDisplay(entry.totalCreditUsd)}
-
-
-
-
- )
-}
-
-export function UsageLog({ address, backendUrl, refreshSignal = 0 }: UsageLogProps) {
- const [expanded, setExpanded] = useState(false)
- const [showAll, setShowAll] = useState(false)
- const [entries, setEntries] = useState([])
- const [loading, setLoading] = useState(false)
-
- useEffect(() => {
- if (!address) {
- setEntries([])
- return
- }
-
- let cancelled = false
- const client = createBackendClient(backendUrl)
- setLoading(true)
-
- void client
- .getUsageLog(address)
- .then((log) => {
- if (!cancelled) {
- setEntries(log)
- setShowAll(false)
- }
- })
- .catch(() => {
- if (!cancelled) {
- setEntries([])
- setShowAll(false)
- }
- })
- .finally(() => {
- if (!cancelled) setLoading(false)
- })
-
- return () => {
- cancelled = true
- }
- }, [address, backendUrl, refreshSignal])
-
- const summary = useMemo(() => {
- if (loading) return 'Loading…'
- if (entries.length === 0) return 'No entries yet'
- return formatFundedSummary(entries)
- }, [entries, loading])
-
- const hiddenCount = Math.max(0, entries.length - INITIAL_VISIBLE_ENTRIES)
- const visibleEntries = showAll ? entries : entries.slice(0, INITIAL_VISIBLE_ENTRIES)
-
- return (
-
- {
- if (expanded) setShowAll(false)
- setExpanded((value) => !value)
- }}
- cursor="pointer"
- >
-
- Credit History
-
-
-
- {summary}
-
-
-
-
-
- {expanded && (
-
- {loading ? (
-
- ) : entries.length === 0 ? (
-
- Purchases and funding activity will appear here.
-
- ) : (
-
- {visibleEntries.map((entry) => (
-
- ))}
- {hiddenCount > 0 && (
- {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault()
- setShowAll((value) => !value)
- }
- }}
- onPress={() => setShowAll((value) => !value)}
- >
- {showAll ? 'Show less' : `Show ${hiddenCount} more`}
-
- )}
-
- )}
-
- )}
-
- )
-}
diff --git a/packages/ai-credits-widget/src/useAiCreditsHistory.ts b/packages/ai-credits-widget/src/useAiCreditsHistory.ts
new file mode 100644
index 00000000..9a23318a
--- /dev/null
+++ b/packages/ai-credits-widget/src/useAiCreditsHistory.ts
@@ -0,0 +1,210 @@
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import type { GdCreditEntry } from './backendTypes'
+import { createBackendClient } from './backendClient'
+
+export const HISTORY_PAGE_SIZE = 10
+export const HISTORY_LOOKBACK_DAYS = 90
+
+export type CreditHistorySource = GdCreditEntry['source']
+export type CreditHistoryStatusFilter = 'all' | GdCreditEntry['fundingStatus']
+
+export const HISTORY_SOURCE_OPTIONS: {
+ id: CreditHistorySource
+ label: string
+ defaultChecked: boolean
+}[] = [
+ { id: 'deposit', label: 'Deposit', defaultChecked: true },
+ { id: 'streamUpdate', label: 'Stream update', defaultChecked: true },
+ { id: 'streamRequest', label: 'Stream credit', defaultChecked: true },
+ { id: 'streamCron', label: 'Stream credit', defaultChecked: true },
+]
+
+export function formatHistoryDateInput(date: Date): string {
+ return date.toISOString().slice(0, 10)
+}
+
+export function getLast90DaysRange(now = new Date()): { from: string; to: string } {
+ const to = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
+ const from = new Date(to)
+ from.setUTCDate(from.getUTCDate() - HISTORY_LOOKBACK_DAYS)
+ return {
+ from: formatHistoryDateInput(from),
+ to: formatHistoryDateInput(to),
+ }
+}
+
+function createDefaultSelectedSources(): Record {
+ return Object.fromEntries(
+ HISTORY_SOURCE_OPTIONS.map((option) => [option.id, option.defaultChecked]),
+ ) as Record
+}
+
+function toIsoStartOfDay(dateValue: string): string | undefined {
+ if (!dateValue) return undefined
+ const parsed = Date.parse(`${dateValue}T00:00:00.000Z`)
+ if (Number.isNaN(parsed)) return undefined
+ return new Date(parsed).toISOString()
+}
+
+function toIsoEndOfDay(dateValue: string): string | undefined {
+ if (!dateValue) return undefined
+ const parsed = Date.parse(`${dateValue}T23:59:59.999Z`)
+ if (Number.isNaN(parsed)) return undefined
+ return new Date(parsed).toISOString()
+}
+
+export interface AiCreditsHistoryState {
+ selectedSources: Record
+ statusFilter: CreditHistoryStatusFilter
+ fromDate: string
+ toDate: string
+ entries: GdCreditEntry[]
+ offset: number
+ hasMore: boolean
+ loading: boolean
+ loadingMore: boolean
+ error: string | null
+ activeSources: CreditHistorySource[]
+}
+
+export interface AiCreditsHistoryActions {
+ setSourceChecked: (source: CreditHistorySource, checked: boolean) => void
+ setStatusFilter: (status: CreditHistoryStatusFilter) => void
+ setFromDate: (value: string) => void
+ setToDate: (value: string) => void
+ reload: () => Promise
+ loadMore: () => Promise
+}
+
+export interface UseAiCreditsHistoryResult {
+ state: AiCreditsHistoryState
+ actions: AiCreditsHistoryActions
+}
+
+export function useAiCreditsHistory(options: {
+ address: string | null
+ backendUrl?: string
+}): UseAiCreditsHistoryResult {
+ const { address, backendUrl } = options
+ const defaultRange = useMemo(() => getLast90DaysRange(), [])
+
+ const [selectedSources, setSelectedSources] = useState(createDefaultSelectedSources)
+ const [statusFilter, setStatusFilter] = useState('all')
+ const [fromDate, setFromDate] = useState(defaultRange.from)
+ const [toDate, setToDate] = useState(defaultRange.to)
+ const [entries, setEntries] = useState([])
+ const [offset, setOffset] = useState(0)
+ const [hasMore, setHasMore] = useState(false)
+ const [loading, setLoading] = useState(false)
+ const [loadingMore, setLoadingMore] = useState(false)
+ const [error, setError] = useState(null)
+
+ const activeSources = useMemo(
+ () => HISTORY_SOURCE_OPTIONS.map((option) => option.id).filter((id) => selectedSources[id]),
+ [selectedSources],
+ )
+
+ const loadHistory = useCallback(
+ async (nextOffset: number, append: boolean) => {
+ if (!address) {
+ setEntries([])
+ setOffset(0)
+ setHasMore(false)
+ setError(null)
+ setLoading(false)
+ setLoadingMore(false)
+ return
+ }
+
+ if (activeSources.length === 0) {
+ setEntries([])
+ setOffset(0)
+ setHasMore(false)
+ setError(null)
+ setLoading(false)
+ setLoadingMore(false)
+ return
+ }
+
+ if (append) setLoadingMore(true)
+ else setLoading(true)
+ setError(null)
+
+ const client = createBackendClient(backendUrl)
+ const apiSource = activeSources.length === 1 ? activeSources[0] : undefined
+ const fundingStatus = statusFilter === 'all' ? undefined : statusFilter
+
+ try {
+ const response = await client.getCreditHistory(address, {
+ limit: HISTORY_PAGE_SIZE,
+ offset: nextOffset,
+ source: apiSource,
+ fundingStatus,
+ from: toIsoStartOfDay(fromDate),
+ to: toIsoEndOfDay(toDate),
+ })
+ const pageItems =
+ activeSources.length === 1
+ ? response.items
+ : response.items.filter((entry) => selectedSources[entry.source])
+
+ setEntries((prev) => (append ? [...prev, ...pageItems] : pageItems))
+ setOffset(nextOffset)
+ setHasMore(response.hasMore)
+ } catch (err: unknown) {
+ if (!append) setEntries([])
+ setHasMore(false)
+ setError(err instanceof Error ? err.message : 'Unable to load credit history')
+ } finally {
+ setLoading(false)
+ setLoadingMore(false)
+ }
+ },
+ [address, backendUrl, activeSources, statusFilter, fromDate, toDate, selectedSources],
+ )
+
+ useEffect(() => {
+ void loadHistory(0, false)
+ }, [loadHistory])
+
+ const setSourceChecked = useCallback((source: CreditHistorySource, checked: boolean) => {
+ setSelectedSources((prev) => {
+ if (source === 'streamRequest' || source === 'streamCron') {
+ return { ...prev, streamRequest: checked, streamCron: checked }
+ }
+ return { ...prev, [source]: checked }
+ })
+ }, [])
+
+ const reload = useCallback(async () => {
+ await loadHistory(0, false)
+ }, [loadHistory])
+
+ const loadMore = useCallback(async () => {
+ await loadHistory(offset + HISTORY_PAGE_SIZE, true)
+ }, [loadHistory, offset])
+
+ return {
+ state: {
+ selectedSources,
+ statusFilter,
+ fromDate,
+ toDate,
+ entries,
+ offset,
+ hasMore,
+ loading,
+ loadingMore,
+ error,
+ activeSources,
+ },
+ actions: {
+ setSourceChecked,
+ setStatusFilter,
+ setFromDate,
+ setToDate,
+ reload,
+ loadMore,
+ },
+ }
+}
diff --git a/packages/ai-credits-widget/src/widgetRuntimeContract.ts b/packages/ai-credits-widget/src/widgetRuntimeContract.ts
index bb1b238f..a3aeebe8 100644
--- a/packages/ai-credits-widget/src/widgetRuntimeContract.ts
+++ b/packages/ai-credits-widget/src/widgetRuntimeContract.ts
@@ -15,7 +15,7 @@ export type AiCreditsWidgetStatus =
| 'backend_unavailable'
| 'unsupported_chain'
-export type AiCreditsWidgetTab = 'buy' | 'manage'
+export type AiCreditsWidgetTab = 'buy' | 'manage' | 'history'
export interface AiCreditsQuote {
depositAmountG: string
diff --git a/packages/ui/src/components/Icon.tsx b/packages/ui/src/components/Icon.tsx
index 19ecfcbc..6ae1dd6c 100644
--- a/packages/ui/src/components/Icon.tsx
+++ b/packages/ui/src/components/Icon.tsx
@@ -53,6 +53,9 @@ const SVG_PATHS: Record = {
'arrow-right': 'M5 12h14M12 5l7 7-7 7',
'arrow-down': 'M12 5v14M19 12l-7 7-7-7',
'arrow-up': 'M12 19V5M5 12l7-7 7 7',
+ plus: 'M12 5v14M5 12h14',
+ 'arrows-left-right': ['M8 7l-5 5 5 5', 'M3 12h18', 'M16 17l5-5-5-5'],
+ 'more-horizontal': 'M5 12h.01M12 12h.01M19 12h.01',
copy: 'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z',
wallet: 'M20 7H4a2 2 0 00-2 2v10a2 2 0 002 2h16a2 2 0 002-2V9a2 2 0 00-2-2zM16 14a1 1 0 110-2 1 1 0 010 2zM4 7V5a2 2 0 012-2h12a2 2 0 012 2v2',
'external-link': 'M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3',
diff --git a/tests/widgets/ai-credits-widget/states.spec.ts b/tests/widgets/ai-credits-widget/states.spec.ts
index e390f2fa..5c6627f0 100644
--- a/tests/widgets/ai-credits-widget/states.spec.ts
+++ b/tests/widgets/ai-credits-widget/states.spec.ts
@@ -14,6 +14,8 @@ const STORY_IDS = {
'/iframe.html?id=qa-aicreditswidget-runtime-fixtures--payment-confirmed&viewMode=story',
creditsManagement:
'/iframe.html?id=qa-aicreditswidget-runtime-fixtures--credits-management&viewMode=story',
+ historyTab:
+ '/iframe.html?id=qa-aicreditswidget-runtime-fixtures--history-tab&viewMode=story',
insufficientBalance:
'/iframe.html?id=qa-aicreditswidget-runtime-fixtures--insufficient-g-balance&viewMode=story',
paymentFailed:
@@ -104,13 +106,30 @@ test('AiCreditsWidget manage tab', async ({ page }) => {
await expect(page.getByTestId('AiCreditsWidget-manage-tab')).toBeVisible()
await expect(page.getByText('Buy Credits')).toBeVisible()
await expect(page.getByText('Manage')).toBeVisible()
+ await expect(page.getByText('History')).toBeVisible()
await expect(page.getByText('110.00')).toBeVisible()
+ await expect(page.getByText('Credit History')).not.toBeVisible()
await page.screenshot({
path: 'tests/widgets/ai-credits-widget/test-results/acw-07-credits-management.png',
fullPage: true,
})
})
+test('AiCreditsWidget history tab', async ({ page }) => {
+ await gotoStory(page, STORY_IDS.historyTab)
+ const root = widget(page, 'AiCreditsWidget-history-tab')
+ await expect(root).toBeVisible()
+ await expect(root.getByText('AI Credit History')).toBeVisible()
+ await expect(root.getByText('Last 90 days activity')).toBeVisible()
+ await expect(root.getByText('Deposit')).toBeVisible()
+ await expect(root.getByText('CREDIT History', { exact: true })).toBeVisible()
+ await expect(root.getByText('G$ Deposit')).toBeVisible({ timeout: 10_000 })
+ await page.screenshot({
+ path: 'tests/widgets/ai-credits-widget/test-results/acw-13-history-tab.png',
+ fullPage: true,
+ })
+})
+
test('AiCreditsWidget insufficient_g_balance', async ({ page }) => {
await gotoStory(page, STORY_IDS.insufficientBalance)
await expect(page.getByTestId('AiCreditsWidget-insufficient-balance')).toBeVisible()
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-02-purchase-setup.png b/tests/widgets/ai-credits-widget/test-results/acw-02-purchase-setup.png
index d979c3e2..b7b0d1f4 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-02-purchase-setup.png and b/tests/widgets/ai-credits-widget/test-results/acw-02-purchase-setup.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-03-quote-ready.png b/tests/widgets/ai-credits-widget/test-results/acw-03-quote-ready.png
index 0785150f..1d4bac60 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-03-quote-ready.png and b/tests/widgets/ai-credits-widget/test-results/acw-03-quote-ready.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-04-quote-ready-goodid.png b/tests/widgets/ai-credits-widget/test-results/acw-04-quote-ready-goodid.png
index f2a24f2d..9fdaf05a 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-04-quote-ready-goodid.png and b/tests/widgets/ai-credits-widget/test-results/acw-04-quote-ready-goodid.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-05-payment-pending.png b/tests/widgets/ai-credits-widget/test-results/acw-05-payment-pending.png
index ce3f7f8b..3f9c4ee5 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-05-payment-pending.png and b/tests/widgets/ai-credits-widget/test-results/acw-05-payment-pending.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-06-payment-confirmed.png b/tests/widgets/ai-credits-widget/test-results/acw-06-payment-confirmed.png
index 6ad7d976..bb4dd8f3 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-06-payment-confirmed.png and b/tests/widgets/ai-credits-widget/test-results/acw-06-payment-confirmed.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-07-credits-management.png b/tests/widgets/ai-credits-widget/test-results/acw-07-credits-management.png
index ae55fa91..daebcef7 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-07-credits-management.png and b/tests/widgets/ai-credits-widget/test-results/acw-07-credits-management.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-08-insufficient-g-balance.png b/tests/widgets/ai-credits-widget/test-results/acw-08-insufficient-g-balance.png
index 1ec4fc33..78418a03 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-08-insufficient-g-balance.png and b/tests/widgets/ai-credits-widget/test-results/acw-08-insufficient-g-balance.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-09-payment-failed.png b/tests/widgets/ai-credits-widget/test-results/acw-09-payment-failed.png
index f823651b..36dd7566 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-09-payment-failed.png and b/tests/widgets/ai-credits-widget/test-results/acw-09-payment-failed.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-10-backend-unavailable.png b/tests/widgets/ai-credits-widget/test-results/acw-10-backend-unavailable.png
index d70440f3..80f9c132 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-10-backend-unavailable.png and b/tests/widgets/ai-credits-widget/test-results/acw-10-backend-unavailable.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-11-unsupported-chain.png b/tests/widgets/ai-credits-widget/test-results/acw-11-unsupported-chain.png
index fa18d0ad..9e1a6cd2 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-11-unsupported-chain.png and b/tests/widgets/ai-credits-widget/test-results/acw-11-unsupported-chain.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-13-history-tab.png b/tests/widgets/ai-credits-widget/test-results/acw-13-history-tab.png
new file mode 100644
index 00000000..4e62a35d
Binary files /dev/null and b/tests/widgets/ai-credits-widget/test-results/acw-13-history-tab.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-before.png b/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-before.png
index 245a1674..784ac2a0 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-before.png and b/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-before.png differ
diff --git a/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-modal-open.png b/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-modal-open.png
index 245a1674..784ac2a0 100644
Binary files a/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-modal-open.png and b/tests/widgets/ai-credits-widget/test-results/acw-14-appkit-connect-modal-open.png differ