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
26 changes: 23 additions & 3 deletions src/__tests__/crypto-checkout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { describe, expect, it, vi } from "vitest";

vi.mock("framer-motion", () => ({
motion: {
div: ({ children, ...props }: Record<string, unknown>) => <div {...props}>{children}</div>,
div: ({ children, ...props }: { children?: React.ReactNode; [key: string]: unknown }) => (
<div {...props}>{children}</div>
),
},
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
Expand All @@ -15,8 +17,26 @@ vi.mock("qrcode.react", () => ({

vi.mock("@/lib/api", () => ({
getSupportedPaymentMethods: vi.fn().mockResolvedValue([
{ id: "BTC:mainnet", type: "native", token: "BTC", chain: "bitcoin", displayName: "Bitcoin", decimals: 8, displayOrder: 0, iconUrl: "" },
{ id: "USDT:tron", type: "erc20", token: "USDT", chain: "tron", displayName: "USDT on Tron", decimals: 6, displayOrder: 1, iconUrl: "" },
{
id: "BTC:mainnet",
type: "native",
token: "BTC",
chain: "bitcoin",
displayName: "Bitcoin",
decimals: 8,
displayOrder: 0,
iconUrl: "",
},
{
id: "USDT:tron",
type: "erc20",
token: "USDT",
chain: "tron",
displayName: "USDT on Tron",
decimals: 6,
displayOrder: 1,
iconUrl: "",
},
]),
createCheckout: vi.fn().mockResolvedValue({
depositAddress: "THwbQb1sPiRUpUYunVQxQKc6i4LCmpP1mj",
Expand Down
4 changes: 2 additions & 2 deletions src/app/(dashboard)/billing/credits/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { AutoTopupCard } from "@/components/billing/auto-topup-card";
import { BuyCreditsPanel } from "@/components/billing/buy-credits-panel";
import { BuyCryptoCreditPanel } from "@/components/billing/buy-crypto-credits-panel";
import { CouponInput } from "@/components/billing/coupon-input";
import { CreditBalance } from "@/components/billing/credit-balance";
import { CryptoCheckout } from "@/components/billing/crypto-checkout";
import { DividendBanner } from "@/components/billing/dividend-banner";
import { DividendEligibility } from "@/components/billing/dividend-eligibility";
import { DividendPoolStats } from "@/components/billing/dividend-pool-stats";
Expand Down Expand Up @@ -200,7 +200,7 @@ function CreditsContent() {

<BuyCreditsPanel />
<CouponInput />
<BuyCryptoCreditPanel />
<CryptoCheckout />
<AutoTopupCard />
<TransactionHistory />

Expand Down
28 changes: 21 additions & 7 deletions src/components/billing/crypto-checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { useCallback, useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
type CheckoutResult,
type SupportedPaymentMethod,
createCheckout,
getChargeStatus,
getSupportedPaymentMethods,
type SupportedPaymentMethod,
} from "@/lib/api";
import { AmountSelector } from "./amount-selector";
import { ConfirmationTracker } from "./confirmation-tracker";
Expand All @@ -30,7 +30,9 @@ export function CryptoCheckout() {
const [loading, setLoading] = useState(false);

useEffect(() => {
getSupportedPaymentMethods().then(setMethods).catch(() => {});
getSupportedPaymentMethods()
.then(setMethods)
.catch(() => {});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Swallowing errors in the payment methods fetch makes debugging and UX issues harder to detect.

This empty catch means network or backend failures when loading payment methods are ignored, potentially resulting in an empty UI (methods.length === 0) with no signal of the underlying issue. Please at least log the error (e.g., Sentry/console) or show a simple fallback state so production issues are detectable.

}, []);

useEffect(() => {
Expand All @@ -47,7 +49,10 @@ export function CryptoCheckout() {
} else if (res.status === "expired" || res.status === "failed") {
setStatus(res.status as PaymentStatus);
clearInterval(interval);
} else if (res.amountReceivedCents > 0 && res.amountReceivedCents >= res.amountExpectedCents) {
} else if (
res.amountReceivedCents > 0 &&
res.amountReceivedCents >= res.amountExpectedCents
) {
setStatus("confirming");
setStep("confirming");
} else if (res.amountReceivedCents > 0) {
Comment on lines +52 to 58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant setStep("confirming") on every poll

Once the full amount is received but not yet credited, the condition amountReceivedCents >= amountExpectedCents remains true on every subsequent poll tick. This causes setStep("confirming") and setStatus("confirming") to be called redundantly on every 5-second interval, generating unnecessary re-renders.

Suggested change
} else if (
res.amountReceivedCents > 0 &&
res.amountReceivedCents >= res.amountExpectedCents
) {
setStatus("confirming");
setStep("confirming");
} else if (res.amountReceivedCents > 0) {
} else if (
res.amountReceivedCents > 0 &&
res.amountReceivedCents >= res.amountExpectedCents
) {
setStatus((prev) => (prev !== "confirming" ? "confirming" : prev));
setStep((prev) => (prev !== "confirming" ? "confirming" : prev));
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/billing/crypto-checkout.tsx
Line: 52-58

Comment:
**Redundant `setStep("confirming")` on every poll**

Once the full amount is received but not yet credited, the condition `amountReceivedCents >= amountExpectedCents` remains true on every subsequent poll tick. This causes `setStep("confirming")` and `setStatus("confirming")` to be called redundantly on every 5-second interval, generating unnecessary re-renders.

```suggestion
        } else if (
          res.amountReceivedCents > 0 &&
          res.amountReceivedCents >= res.amountExpectedCents
        ) {
          setStatus((prev) => (prev !== "confirming" ? "confirming" : prev));
          setStep((prev) => (prev !== "confirming" ? "confirming" : prev));
```

How can I resolve this? If you propose a fix, please make it concise.

Expand Down Expand Up @@ -93,7 +98,11 @@ export function CryptoCheckout() {
if (methods.length === 0) return null;

return (
<motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Expand Down Expand Up @@ -122,12 +131,13 @@ export function CryptoCheckout() {
>
<PaymentMethodPicker
methods={methods}
amountUsd={amountUsd}
onSelect={handleMethod}
onBack={() => setStep("amount")}
/>
{loading && (
<p className="mt-2 text-xs text-muted-foreground animate-pulse">Creating checkout...</p>
<p className="mt-2 text-xs text-muted-foreground animate-pulse">
Creating checkout...
</p>
)}
</motion.div>
)}
Expand Down Expand Up @@ -155,7 +165,11 @@ export function CryptoCheckout() {
credited={status === "credited"}
/>
{status === "credited" && (
<button type="button" onClick={handleReset} className="mt-4 text-sm text-primary hover:underline">
<button
type="button"
onClick={handleReset}
className="mt-4 text-sm text-primary hover:underline"
>
Done — buy more credits
</button>
)}
Expand Down
Loading