mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(agent-world): re-enable Buy after a purchase in Tiny Place trading (#4217)
Co-authored-by: M3gA-Mind <elvin@mahadao.com>
This commit is contained in:
@@ -1046,6 +1046,77 @@ describe('Trading tab — buy identity (x402)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Buy buttons re-enable after a purchase and Dismiss clears the banner (#4196)', async () => {
|
||||
vi.mocked(apiClient.marketplace.buyIdentity)
|
||||
.mockResolvedValueOnce({
|
||||
challenge: { amount: '20000000', asset: 'USDC', network: 'solana-devnet' },
|
||||
walletBalance: { raw: '50000000', formatted: '50', decimals: 6, assetSymbol: 'USDC' },
|
||||
walletAddress: 'WalletXyz12345678',
|
||||
})
|
||||
.mockResolvedValueOnce({ result: { saleId: 's1' }, payment: { onChainTx: 'TxId1' } });
|
||||
render(<IdentitiesSection />);
|
||||
await gotoTab('Trading');
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Buy' }));
|
||||
await userEvent.click(await screen.findByTestId('x402-confirm'));
|
||||
|
||||
// Purchase completed.
|
||||
await screen.findByTestId('buy-identity-success');
|
||||
|
||||
// Regression: the Buy button must NOT stay permanently disabled after a
|
||||
// completed purchase (the old `disabled={buying !== null}` never reset
|
||||
// because `closeBuy` was only reachable from the now-unmounted dialog).
|
||||
expect(screen.getByRole('button', { name: 'Buy' })).toBeEnabled();
|
||||
|
||||
// Dismiss clears the terminal banner and keeps Buy usable.
|
||||
await userEvent.click(screen.getByTestId('buy-identity-dismiss'));
|
||||
expect(screen.queryByTestId('buy-identity-success')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Buy' })).toBeEnabled();
|
||||
});
|
||||
|
||||
test('a failed purchase shows the error banner and Dismiss resets it (#4196)', async () => {
|
||||
vi.mocked(apiClient.marketplace.buyIdentity)
|
||||
.mockResolvedValueOnce({
|
||||
challenge: { amount: '20000000', asset: 'USDC', network: 'solana-devnet' },
|
||||
walletBalance: { raw: '50000000', formatted: '50', decimals: 6, assetSymbol: 'USDC' },
|
||||
walletAddress: 'WalletXyz12345678',
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('settlement boom'));
|
||||
render(<IdentitiesSection />);
|
||||
await gotoTab('Trading');
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Buy' }));
|
||||
await userEvent.click(await screen.findByTestId('x402-confirm'));
|
||||
|
||||
const errorBanner = await screen.findByTestId('buy-identity-error');
|
||||
expect(errorBanner).toHaveTextContent('Purchase failed.');
|
||||
|
||||
// Buy must re-enable after a failed attempt, and Dismiss clears the banner.
|
||||
expect(screen.getByRole('button', { name: 'Buy' })).toBeEnabled();
|
||||
await userEvent.click(screen.getByTestId('buy-identity-dismiss'));
|
||||
expect(screen.queryByTestId('buy-identity-error')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Buy' })).toBeEnabled();
|
||||
});
|
||||
|
||||
test('a post-payment failure surfaces the broadcast tx in the buy error banner (#4196)', async () => {
|
||||
// The settlement broadcast went out but the purchase did not finalize: the
|
||||
// error carries an `onChainTx=`, so the banner must report "Payment sent…"
|
||||
// (the truthy branch of the buy error message) and re-enable Buy.
|
||||
vi.mocked(apiClient.marketplace.buyIdentity)
|
||||
.mockResolvedValueOnce({
|
||||
challenge: { amount: '20000000', asset: 'USDC', network: 'solana-devnet' },
|
||||
walletBalance: { raw: '50000000', formatted: '50', decimals: 6, assetSymbol: 'USDC' },
|
||||
walletAddress: 'WalletXyz12345678',
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('paid but not confirmed (onChainTx=BuyTx99)'));
|
||||
render(<IdentitiesSection />);
|
||||
await gotoTab('Trading');
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Buy' }));
|
||||
await userEvent.click(await screen.findByTestId('x402-confirm'));
|
||||
|
||||
const errorBanner = await screen.findByTestId('buy-identity-error');
|
||||
expect(errorBanner).toHaveTextContent('Payment sent but purchase did not complete.');
|
||||
expect(screen.getByRole('button', { name: 'Buy' })).toBeEnabled();
|
||||
});
|
||||
|
||||
test('auction listings do not show a Buy button', async () => {
|
||||
vi.mocked(apiClient.marketplace.listIdentities).mockResolvedValue({
|
||||
identities: [{ ...fixedListing, listingType: 'auction' as const }],
|
||||
|
||||
@@ -707,6 +707,13 @@ function TradingTab() {
|
||||
const [buying, setBuying] = useState<IdentityListing | null>(null);
|
||||
const buy = useX402Buy((id, opts) => apiClient.marketplace.buyIdentity(id, opts));
|
||||
const bs = buy.state;
|
||||
// A buy is "in flight" only while the probe/confirm/pay round-trip is active.
|
||||
// Once it reaches a terminal banner (success/error) the Buy buttons must
|
||||
// re-enable so the user can buy again. Otherwise `buying` stays set forever —
|
||||
// `closeBuy` is only reachable from the dialog, which unmounts on success/error
|
||||
// — and `disabled={buying !== null}` leaves every Buy button permanently
|
||||
// greyed out after the first attempt (#4196).
|
||||
const buyInFlight = buying !== null && bs.phase !== 'success' && bs.phase !== 'error';
|
||||
function startBuy(listing: IdentityListing) {
|
||||
setBuying(listing);
|
||||
buy.begin(listing.listingId);
|
||||
@@ -810,7 +817,7 @@ function TradingTab() {
|
||||
variant="primary"
|
||||
size="xs"
|
||||
className="flex-1"
|
||||
disabled={buying !== null}
|
||||
disabled={buyInFlight}
|
||||
onClick={() => startBuy(listing)}>
|
||||
Buy
|
||||
</Button>
|
||||
@@ -876,7 +883,16 @@ function TradingTab() {
|
||||
<div
|
||||
className="mt-3 rounded-md border border-green-500/30 bg-green-500/10 p-3"
|
||||
data-testid="buy-identity-success">
|
||||
<p className="text-xs font-medium text-green-500">Purchased {buying.name}</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-xs font-medium text-green-500">Purchased {buying.name}</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
onClick={closeBuy}
|
||||
data-testid="buy-identity-dismiss">
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
{bs.onChainTx && (
|
||||
<a
|
||||
href={buyExplorerTxUrl(bs.onChainTx, bs.network)}
|
||||
@@ -892,9 +908,18 @@ function TradingTab() {
|
||||
<div
|
||||
className="mt-3 rounded-md border border-red-500/30 bg-red-500/10 p-3"
|
||||
data-testid="buy-identity-error">
|
||||
<p className="text-xs font-medium text-red-500">
|
||||
{bs.onChainTx ? 'Payment sent but purchase did not complete.' : 'Purchase failed.'}
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-xs font-medium text-red-500">
|
||||
{bs.onChainTx ? 'Payment sent but purchase did not complete.' : 'Purchase failed.'}
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
onClick={closeBuy}
|
||||
data-testid="buy-identity-dismiss">
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-content-muted">{bs.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user