Skip to content

feat: crypto checkout redesign — 4-step flow with search, QR, confirmation#62

Open
TSavo wants to merge 7 commits intomainfrom
feat/product-config-admin
Open

feat: crypto checkout redesign — 4-step flow with search, QR, confirmation#62
TSavo wants to merge 7 commits intomainfrom
feat/product-config-admin

Conversation

@TSavo
Copy link
Contributor

@TSavo TSavo commented Mar 24, 2026

Summary

  • Replace horizontal-tab crypto checkout with 4-step flow that scales to 25+ payment methods
  • Searchable flat list with Popular/Stablecoins/L2/Native filter pills
  • QR code deposit addresses with countdown timer
  • Confirmation progress bar with step timeline
  • Old BuyCryptoCreditPanel re-exported for backward compatibility

Components

  • AmountSelector — preset ($10-$100) + custom input
  • PaymentMethodPicker — search + filter pills over dynamic method list
  • DepositView — QR code, address copy, live countdown, status indicator
  • ConfirmationTracker — progress bar, 3-step timeline, auto-redirect
  • CryptoCheckout — parent step state machine

Test plan

  • 18 tests across 5 test files, all passing
  • AmountSelector: presets, custom, validation, disabled state
  • PaymentMethodPicker: search, filter pills, selection callback
  • DepositView: address display, QR, clipboard, status
  • ConfirmationTracker: progress, credited state, progressbar role
  • CryptoCheckout: mount renders amount, advances to picker
  • No backend changes — consumes existing /chains and /charges APIs

🤖 Generated with Claude Code

Summary by Sourcery

Redesign the crypto credits checkout into a multi-step, extensible flow and introduce an admin UI for managing product branding, navigation, features, fleet, and billing configuration.

New Features:

  • Introduce a four-step CryptoCheckout flow with amount selection, payment method search, deposit QR view, and confirmation tracking for on-chain payments.
  • Add an admin Product Configuration page with tabbed forms to manage brand settings, navigation items, feature flags, fleet configuration, and billing parameters from the platform API.
  • Expose a brand configuration bootstrap helper that fetches brand settings from the platform API and applies them at runtime.

Enhancements:

  • Refactor the legacy BuyCryptoCreditPanel to re-export the new CryptoCheckout component for backward compatibility with existing callers.

Tests:

  • Add unit tests for the crypto checkout subcomponents, including payment method filtering and deposit view behaviors, to validate the new flow and UI pieces.

Note

Add 4-step crypto checkout flow with amount selection, payment method search, QR deposit, and confirmation tracking

  • Replaces the inline BuyCryptoCreditPanel with a new CryptoCheckout component that guides users through amount selection → payment method → deposit instructions → confirmation.
  • AmountSelector offers preset amounts ($10–$100) and a custom input; PaymentMethodPicker provides searchable, filterable crypto method selection.
  • DepositView shows a QR code, copy-to-clipboard address, and a countdown timer; ConfirmationTracker polls charge status and displays a progress bar through detection, confirming, and credited states.
  • Also adds an admin Products page with tabbed forms for editing brand, navigation, features, fleet, and billing config via TRPC endpoints.
  • Behavioral Change: BuyCryptoCreditPanel now renders the multi-step checkout UI instead of its previous inline implementation.
📊 Macroscope summarized 45567cf. 3 files reviewed, 2 issues evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues

WOPR and others added 6 commits March 23, 2026 20:21
…orms

Tab-based admin page at /admin/products for managing product configuration.
Uses direct fetch to tRPC endpoints (REST-style) to avoid type dependency
on unwired backend router. Five sub-components cover Brand, Navigation,
Features, Fleet, and Billing with toast feedback and disabled-while-saving.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fetches brand config from tRPC endpoint at app init, falls back
to env var defaults if API unavailable. 60s revalidation cache.
Product backends serve this via product.getBrandConfig endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mutateProductConfig checks tRPC-level errors (not just HTTP status)
- initBrandConfig includes credentials, handles non-JSON responses
- Numeric inputs handle empty/NaN values
- Remove dead loading.tsx (client component handles own loading)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r, DepositView, ConfirmationTracker

Four independent components for the 4-step crypto checkout flow:
- AmountSelector: preset + custom amount, min $10
- PaymentMethodPicker: searchable list with Popular/Stablecoins/L2/Native filters
- DepositView: QR code, address copy, countdown timer
- ConfirmationTracker: progress bar, step timeline

16 tests passing across 4 test files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CryptoCheckout manages step state machine (amount → method → deposit → confirming)
- Polls charge status every 5s, auto-advances to confirmation step
- Old BuyCryptoCreditPanel replaced with thin re-export (preserves imports)
- 18 tests across 5 files, all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 24, 2026

Warning

Rate limit exceeded

@TSavo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 33 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7fac2422-cf1e-4627-8a40-5ea65c37fc7b

📥 Commits

Reviewing files that changed from the base of the PR and between 133548d and 45567cf.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • src/__tests__/amount-selector.test.tsx
  • src/__tests__/confirmation-tracker.test.tsx
  • src/__tests__/crypto-checkout.test.tsx
  • src/__tests__/deposit-view.test.tsx
  • src/__tests__/payment-method-picker.test.tsx
  • src/app/admin/products/error.tsx
  • src/app/admin/products/page.tsx
  • src/components/admin/products/billing-form.tsx
  • src/components/admin/products/brand-form.tsx
  • src/components/admin/products/features-form.tsx
  • src/components/admin/products/fleet-form.tsx
  • src/components/admin/products/nav-editor.tsx
  • src/components/billing/amount-selector.tsx
  • src/components/billing/buy-crypto-credits-panel.tsx
  • src/components/billing/confirmation-tracker.tsx
  • src/components/billing/crypto-checkout.tsx
  • src/components/billing/deposit-view.tsx
  • src/components/billing/payment-method-picker.tsx
  • src/lib/brand-config.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/product-config-admin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 24, 2026

Reviewer's Guide

Replaces the legacy BuyCryptoCreditPanel with a new step-based CryptoCheckout flow (amount selection, payment method search/filter, QR deposit, confirmation tracker) while adding an admin Product Configuration UI and a brand config bootstrap helper that consumes platform API configuration.

Sequence diagram for the new CryptoCheckout 4-step flow

sequenceDiagram
  actor User
  participant CryptoCheckout
  participant AmountSelector
  participant PaymentMethodPicker
  participant DepositView
  participant ConfirmationTracker
  participant Api as PlatformApi

  User->>CryptoCheckout: Open credits billing panel
  CryptoCheckout->>PlatformApi: getSupportedPaymentMethods()
  PlatformApi-->>CryptoCheckout: SupportedPaymentMethod[]

  CryptoCheckout->>AmountSelector: Render amount step
  User->>AmountSelector: Select preset or enter custom amount
  AmountSelector-->>CryptoCheckout: onSelect(amountUsd)
  CryptoCheckout->>CryptoCheckout: setStep("method")

  CryptoCheckout->>PaymentMethodPicker: Render method picker
  User->>PaymentMethodPicker: Search/filter methods
  User->>PaymentMethodPicker: Choose SupportedPaymentMethod
  PaymentMethodPicker-->>CryptoCheckout: onSelect(method)

  CryptoCheckout->>PlatformApi: createCheckout(method.id, amountUsd)
  PlatformApi-->>CryptoCheckout: CheckoutResult
  CryptoCheckout->>CryptoCheckout: setStep("deposit"), status="waiting"

  CryptoCheckout->>DepositView: Render QR deposit view
  User->>DepositView: Scan QR or copy address

  loop Poll charge status
    CryptoCheckout->>PlatformApi: getChargeStatus(referenceId)
    PlatformApi-->>CryptoCheckout: chargeStatus
    CryptoCheckout->>CryptoCheckout: update status, confirmations
    alt amountReceivedCents >= amountExpectedCents
      CryptoCheckout->>CryptoCheckout: setStep("confirming"), status="confirming"
    end
  end

  CryptoCheckout->>ConfirmationTracker: Render confirmation tracker
  ConfirmationTracker->>ConfirmationTracker: Update progress bar from confirmations
  alt credited
    CryptoCheckout->>CryptoCheckout: status="credited"
    User->>CryptoCheckout: Click "Done — buy more credits"
    CryptoCheckout->>CryptoCheckout: Reset state to step="amount"
  else expired or failed
    CryptoCheckout->>CryptoCheckout: status="expired" or "failed"
    User->>CryptoCheckout: Navigate back or restart
  end
Loading

Updated class diagram for CryptoCheckout and billing UI components

classDiagram
  class CryptoCheckout {
    -Step step
    -SupportedPaymentMethod[] methods
    -number amountUsd
    -CheckoutResult checkout
    -PaymentStatus status
    -number confirmations
    -number confirmationsRequired
    -boolean loading
    +CryptoCheckout()
    +handleAmount(amountUsd number) void
    +handleMethod(method SupportedPaymentMethod) Promise~void~
    +handleReset() void
  }

  class AmountSelector {
    -number selected
    -string custom
    +AmountSelector(onSelect function)
    +onSelect(amount number) void
  }

  class PaymentMethodPicker {
    -SupportedPaymentMethod[] methods
    -number amountUsd
    -string search
    -Filter filter
    +PaymentMethodPicker(methods SupportedPaymentMethod[], amountUsd number, onSelect function, onBack function)
    +onSelect(method SupportedPaymentMethod) void
    +onBack() void
  }

  class DepositView {
    -CheckoutResult checkout
    -PaymentStatus status
    -boolean copied
    -number timeLeft
    +DepositView(checkout CheckoutResult, status PaymentStatus, onBack function)
    +onBack() void
  }

  class ConfirmationTracker {
    -number confirmations
    -number confirmationsRequired
    -string displayAmount
    -boolean credited
    -string txHash
    +ConfirmationTracker(confirmations number, confirmationsRequired number, displayAmount string, credited boolean, txHash string)
  }

  class BuyCryptoCreditPanel {
    +BuyCryptoCreditPanel()
  }

  CryptoCheckout --> AmountSelector : renders
  CryptoCheckout --> PaymentMethodPicker : renders
  CryptoCheckout --> DepositView : renders
  CryptoCheckout --> ConfirmationTracker : renders

  CryptoCheckout ..> SupportedPaymentMethod : uses
  CryptoCheckout ..> CheckoutResult : uses
  CryptoCheckout ..> PaymentStatus : uses
  PaymentMethodPicker ..> SupportedPaymentMethod : filters
  DepositView ..> CheckoutResult : displays

  class SupportedPaymentMethod {
    +string id
    +string token
    +string chain
    +string displayName
    +string type
    +string iconUrl
  }

  class CheckoutResult {
    +string referenceId
    +string displayAmount
    +string depositAddress
    +string token
    +string chain
    +number amountUsd
  }

  class PaymentStatus {
  }

  class Step {
  }

  class createCheckout {
    +createCheckout(methodId string, amountUsd number) Promise~CheckoutResult~
  }

  class getSupportedPaymentMethods {
    +getSupportedPaymentMethods() Promise~SupportedPaymentMethod[]~
  }

  class getChargeStatus {
    +getChargeStatus(referenceId string) Promise~any~
  }

  CryptoCheckout ..> createCheckout : calls
  CryptoCheckout ..> getSupportedPaymentMethods : calls
  CryptoCheckout ..> getChargeStatus : polls

  BuyCryptoCreditPanel <.. CryptoCheckout : re-export
Loading

Updated class diagram for admin Product Configuration UI and brand init

classDiagram
  class AdminProductsPage {
    -ProductConfig config
    -boolean loading
    -string loadError
    +AdminProductsPage()
    +load() Promise~void~
  }

  class ProductConfig {
    +BrandProduct product
    +NavItem[] navItems
    +FeaturesConfig features
    +FleetConfig fleet
    +BillingConfig billing
  }

  class BrandForm {
    -BrandConfig form
    -boolean saving
    +BrandForm(initial BrandConfig, onSave function)
    +handleSave() Promise~void~
  }

  class NavEditor {
    -NavItem[] items
    -boolean saving
    +NavEditor(initial NavItem[], onSave function)
    +handleSave() Promise~void~
  }

  class FeaturesForm {
    -FeaturesConfig form
    -boolean saving
    +FeaturesForm(initial FeaturesConfig, onSave function)
    +handleSave() Promise~void~
  }

  class FleetForm {
    -FleetConfig form
    -boolean saving
    +FleetForm(initial FleetConfig, onSave function)
    +handleSave() Promise~void~
  }

  class BillingForm {
    -BillingConfig form
    -boolean saving
    +BillingForm(initial BillingConfig, onSave function)
    +handleSave() Promise~void~
  }

  class adminFetch {
    +adminFetch(path string, init RequestInit) Promise~Response~
  }

  class fetchProductConfig {
    +fetchProductConfig() Promise~ProductConfig~
  }

  class mutateProductConfig {
    +mutateProductConfig(endpoint string, input any) Promise~void~
  }

  class BrandConfig {
    +string id
    +string slug
    +string brandName
    +string productName
    +string tagline
    +string domain
    +string appDomain
    +string cookieDomain
    +string companyLegal
    +string priceLabel
    +string defaultImage
    +string emailSupport
    +string emailPrivacy
    +string emailLegal
    +string fromEmail
    +string homePath
    +string storagePrefix
  }

  class FeaturesConfig {
    +boolean chatEnabled
    +boolean onboardingEnabled
    +string onboardingDefaultModel
    +number onboardingMaxCredits
    +string onboardingWelcomeMsg
    +boolean sharedModuleBilling
    +boolean sharedModuleMonitoring
    +boolean sharedModuleAnalytics
  }

  class FleetConfig {
    +string containerImage
    +number containerPort
    +string lifecycle
    +string billingModel
    +number maxInstances
    +string dockerNetwork
    +string placementStrategy
    +string fleetDataDir
  }

  class BillingConfig {
    +string stripePublishableKey
    +Record~string, number~ creditPrices
    +string affiliateBaseUrl
    +string affiliateMatchRate
    +number affiliateMaxCap
    +string dividendRate
  }

  class NavItem {
    +string id
    +string label
    +string href
    +string icon
    +number sortOrder
    +string requiresRole
    +boolean enabled
  }

  AdminProductsPage --> BrandForm : renders
  AdminProductsPage --> NavEditor : renders
  AdminProductsPage --> FeaturesForm : renders
  AdminProductsPage --> FleetForm : renders
  AdminProductsPage --> BillingForm : renders

  AdminProductsPage ..> fetchProductConfig : calls
  AdminProductsPage ..> mutateProductConfig : passes_to_forms

  fetchProductConfig ..> adminFetch : uses
  mutateProductConfig ..> adminFetch : uses

  BrandForm ..> BrandConfig : edits
  NavEditor ..> NavItem : edits
  FeaturesForm ..> FeaturesConfig : edits
  FleetForm ..> FleetConfig : edits
  BillingForm ..> BillingConfig : edits

  class initBrandConfig {
    +initBrandConfig(apiBaseUrl string) Promise~void~
  }

  class setBrandConfig {
    +setBrandConfig(partialConfig Partial~BrandConfig~) void
  }

  initBrandConfig ..> setBrandConfig : applies

  AdminProductsPage ..> ProductConfig : manages
Loading

File-Level Changes

Change Details Files
Replace legacy crypto checkout panel with a modular 4-step CryptoCheckout flow using dedicated subcomponents and improved polling UX.
  • Delete inline implementation of BuyCryptoCreditPanel and re-export new CryptoCheckout component for backward compatibility.
  • Implement CryptoCheckout parent component as a step state machine coordinating amount, method, deposit, and confirmation steps with charge polling.
  • Add AmountSelector for preset and custom USD amounts with validation and disabled state handling.
  • Add PaymentMethodPicker with search box, filter pills (Popular/Stablecoins/L2/Native), and click-to-select behavior over supported methods.
  • Add DepositView with QR code, address copy button, countdown timer, and status-dependent messaging for waiting/partial/expired/failed states.
  • Add ConfirmationTracker displaying confirmations progress bar, 3-step status timeline, and credited state handling.
src/components/billing/buy-crypto-credits-panel.tsx
src/components/billing/crypto-checkout.tsx
src/components/billing/amount-selector.tsx
src/components/billing/payment-method-picker.tsx
src/components/billing/deposit-view.tsx
src/components/billing/confirmation-tracker.tsx
Introduce an admin Product Configuration page with tabbed forms for brand, navigation, features, fleet, and billing, backed by TRPC admin endpoints.
  • Create /admin/products page that fetches product config via PLATFORM_BASE_URL TRPC endpoints and renders tabbed sections.
  • Add BrandForm for editing core brand/product metadata and persisting via updateBrand mutation.
  • Add NavEditor for managing nav items (reorder, enable/disable, role requirement, icon, href) and persisting via updateNavItems.
  • Add FeaturesForm for feature flags and onboarding settings persisted via updateFeatures.
  • Add FleetForm for container/fleet deployment settings (image, ports, lifecycle, billing model, placement, network, data dir) via updateFleet.
  • Add BillingForm for billing parameters including Stripe key, credit price tiers, affiliate settings, and dividend rate via updateBilling.
  • Provide sane default configs for optional sections and loading/error/skeleton states for the admin page.
src/app/admin/products/page.tsx
src/components/admin/products/brand-form.tsx
src/components/admin/products/nav-editor.tsx
src/components/admin/products/features-form.tsx
src/components/admin/products/fleet-form.tsx
src/components/admin/products/billing-form.tsx
src/app/admin/products/error.tsx
Add a helper to initialize brand configuration from the platform API at startup, falling back to env-based defaults.
  • Implement initBrandConfig(apiBaseUrl) to fetch product.getBrandConfig via TRPC, parse JSON defensively, and call setBrandConfig on success.
  • Ensure function is safe against non-JSON and network failures by swallowing errors and leaving env defaults intact.
src/lib/brand-config.ts
Extend test coverage for the new crypto checkout building blocks (picker, deposit view, amount selector, confirmation tracker, and parent flow).
  • Add tests for PaymentMethodPicker covering rendering, search, filter pills, and onSelect callback behavior.
  • Add tests for DepositView verifying QR rendering, address display, waiting status, and clipboard copy behavior.
  • Stub test files for AmountSelector, ConfirmationTracker, and CryptoCheckout to be implemented with detailed scenarios.
src/__tests__/payment-method-picker.test.tsx
src/__tests__/deposit-view.test.tsx
src/__tests__/amount-selector.test.tsx
src/__tests__/confirmation-tracker.test.tsx
src/__tests__/crypto-checkout.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 5 issues, and left some high level feedback:

  • In PaymentMethodPicker the amountUsd prop is currently unused; either wire it into the UI (e.g., show approximate cost per method) or remove the prop to avoid confusion and keep the component interface minimal.
  • When getSupportedPaymentMethods fails in CryptoCheckout, the component returns null, which silently hides the panel; consider rendering a small inline error or fallback state so users understand why crypto checkout is unavailable instead of seeing nothing.
  • In DepositView and other clipboard usages, you call navigator.clipboard.writeText without capability checks; consider guarding with a feature check or catching and surfacing errors so unsupported browsers or restricted contexts degrade more gracefully.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PaymentMethodPicker` the `amountUsd` prop is currently unused; either wire it into the UI (e.g., show approximate cost per method) or remove the prop to avoid confusion and keep the component interface minimal.
- When `getSupportedPaymentMethods` fails in `CryptoCheckout`, the component returns `null`, which silently hides the panel; consider rendering a small inline error or fallback state so users understand why crypto checkout is unavailable instead of seeing nothing.
- In `DepositView` and other clipboard usages, you call `navigator.clipboard.writeText` without capability checks; consider guarding with a feature check or catching and surfacing errors so unsupported browsers or restricted contexts degrade more gracefully.

## Individual Comments

### Comment 1
<location path="src/components/billing/crypto-checkout.tsx" line_range="68-77" />
<code_context>
+    setStep("method");
+  }, []);
+
+  const handleMethod = useCallback(
+    async (method: SupportedPaymentMethod) => {
+      setLoading(true);
+      try {
+        const result = await createCheckout(method.id, amountUsd);
+        setCheckout(result);
+        setStatus("waiting");
+        setStep("deposit");
+      } catch {
+        // Stay on method step
+      } finally {
+        setLoading(false);
+      }
+    },
+    [amountUsd],
+  );
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Errors from `createCheckout` are swallowed, leaving the user without feedback if checkout creation fails.

The empty `catch` in `handleMethod` means `createCheckout` failures (e.g. network/server errors) are completely silent—the user just stays on the method step with no feedback. Consider surfacing an error (local error state or toast), and ensure loading is cleared with an obvious way to retry so failures aren’t invisible.
</issue_to_address>

### Comment 2
<location path="src/components/admin/products/features-form.tsx" line_range="40-42" />
<code_context>
+    setForm((prev) => ({ ...prev, [key]: value || null }));
+  }
+
+  function setNum(key: keyof FeaturesConfig, value: string) {
+    const n = Number.parseInt(value, 10);
+    if (!Number.isNaN(n)) setForm((prev) => ({ ...prev, [key]: n }));
+  }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Numeric onboarding fields cannot be cleared back to an empty/zero state by deleting the input, which may be surprising UX.

In `setNum`, clearing the input (`value === ""`) leads to `Number.parseInt` returning `NaN`, so the state isn’t updated and the previous value remains. Consider handling `""` explicitly (e.g., map it to `0` or `null`), or allow setting the state even when the parsed value is `NaN` and normalize the value on submit, so users can truly clear the field.

Suggested implementation:

```typescript
  function setStr(key: keyof FeaturesConfig, value: string) {
    setForm((prev) => ({ ...prev, [key]: value || null }));
  }

  function setNum(key: keyof FeaturesConfig, value: string) {
    if (value === "") {
      // Allow clearing numeric fields by mapping an empty input to null
      setForm((prev) => ({ ...prev, [key]: null }));
      return;
    }

    const n = Number.parseInt(value, 10);
    if (!Number.isNaN(n)) {
      setForm((prev) => ({ ...prev, [key]: n }));
    }
  }

import { useState } from "react";

```

1. Ensure the corresponding numeric fields in `FeaturesConfig` are typed as `number | null` (or a compatible union) to accept the `null` value used when clearing input.
2. If there is a submit/normalization layer that expects a different cleared value (e.g., `0` instead of `null`), adjust the empty-string mapping in `setNum` accordingly.
</issue_to_address>

### Comment 3
<location path="src/__tests__/payment-method-picker.test.tsx" line_range="82-86" />
<code_context>
+    expect(screen.queryByText("Bitcoin")).not.toBeInTheDocument();
+  });
+
+  it("calls onSelect when a method is clicked", async () => {
+    const onSelect = vi.fn();
+    render(<PaymentMethodPicker methods={METHODS} amountUsd={25} onSelect={onSelect} />);
+    await userEvent.click(screen.getByText("Bitcoin"));
+    expect(onSelect).toHaveBeenCalledWith(METHODS[0]);
+  });
+});
</code_context>
<issue_to_address>
**suggestion (testing):** Consider testing the optional `onBack` behavior when provided

A focused test that renders `PaymentMethodPicker` with `onBack={vi.fn()}`, clicks the `← Back` button, and asserts the callback is called would help ensure the back navigation remains wired correctly in the multi-step flow.
</issue_to_address>

### Comment 4
<location path="src/__tests__/deposit-view.test.tsx" line_range="19-28" />
<code_context>
+describe("DepositView", () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for non-waiting statuses and back navigation in DepositView

Current tests only cover the waiting/QR/copy flow. Please also add tests for `status="partial"`, `"expired"`, and `"failed"` to verify the expected text and indicator styles, and a test that the `&larr; Back` button triggers the `onBack` callback. This will cover all user-visible states and navigation behavior for the deposit step.

Suggested implementation:

```typescript
describe("DepositView", () => {
  it("shows deposit address and amount", () => {
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
    expect(screen.getByText(/32\.24 TRX/)).toBeInTheDocument();
    expect(screen.getByText(/THwbQb1s/)).toBeInTheDocument();
  });

  it("shows waiting status", () => {
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
    expect(screen.getByText(/waiting for payment/i)).toBeInTheDocument();
  });

  it("shows partial status", () => {
    render(<DepositView checkout={CHECKOUT} status="partial" onBack={vi.fn()} />);
    expect(screen.getByText(/payment partially received/i)).toBeInTheDocument();
  });

  it("shows expired status", () => {
    render(<DepositView checkout={CHECKOUT} status="expired" onBack={vi.fn()} />);
    expect(screen.getByText(/payment request expired/i)).toBeInTheDocument();
  });

  it("shows failed status", () => {
    render(<DepositView checkout={CHECKOUT} status="failed" onBack={vi.fn()} />);
    expect(screen.getByText(/payment failed/i)).toBeInTheDocument();
  });

  it("calls onBack when back button is clicked", () => {
    const handleBack = vi.fn();
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={handleBack} />);

    const backButton = screen.getByRole("button", { name: /\s*back/i });
    fireEvent.click(backButton);

    expect(handleBack).toHaveBeenCalledTimes(1);
  });

```

The new tests assume:
1. The UI strings are:
   - `"payment partially received"` for `status="partial"`,
   - `"payment request expired"` for `status="expired"`,
   - `"payment failed"` for `status="failed"`.
   If `DepositView` uses different copy, adjust the regexes in `getByText` to match the actual text.
2. The back button is rendered as a `<button>` with accessible name containing `"← Back"`. If the arrow or casing differ, tweak the `name` matcher (e.g. `/back/i`), or use `getByText`/`getByRole` with the correct label.
3. If you expose status-specific indicator styles via test IDs or class names (e.g. `data-testid="status-indicator"`), you can extend each status test to assert the correct indicator styling (e.g. `expect(indicator).toHaveClass("status-partial")`).
</issue_to_address>

### Comment 5
<location path="src/__tests__/deposit-view.test.tsx" line_range="26-28" />
<code_context>
+    expect(screen.getByText(/THwbQb1s/)).toBeInTheDocument();
+  });
+
+  it("shows waiting status", () => {
+    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
+    expect(screen.getByText(/waiting for payment/i)).toBeInTheDocument();
+  });
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider testing the countdown timer behavior and its stopping conditions

Because `DepositView` keeps a `timeLeft` countdown while `status === "waiting"`, please add a fake-timer test that:
- Advances timers and asserts the MM:SS display updates correctly.
- Asserts that with `status !== "waiting"`, the timer does not keep decrementing.
This will help catch regressions in countdown logic and interval cleanup.

Suggested implementation:

```typescript
  it("shows deposit address and amount", () => {
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
    expect(screen.getByText(/32\.24 TRX/)).toBeInTheDocument();
    expect(screen.getByText(/THwbQb1s/)).toBeInTheDocument();
  });

  it("updates countdown timer while waiting", () => {
    vi.useFakeTimers();

    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);

    const getTimeLeft = () => screen.getByTestId("countdown-timer").textContent;

    const initialTime = getTimeLeft();

    vi.advanceTimersByTime(5_000);

    expect(getTimeLeft()).not.toBe(initialTime);

    vi.useRealTimers();
  });

  it("stops countdown timer when status is no longer waiting", () => {
    vi.useFakeTimers();

    const { rerender } = render(
      <DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />,
    );

    const getTimeLeft = () => screen.getByTestId("countdown-timer").textContent;

    vi.advanceTimersByTime(5_000);
    const timeAfterWaiting = getTimeLeft();

    rerender(<DepositView checkout={CHECKOUT} status="completed" onBack={vi.fn()} />);

    vi.advanceTimersByTime(5_000);

    expect(getTimeLeft()).toBe(timeAfterWaiting);

    vi.useRealTimers();
  });


vi.mock("qrcode.react", () => ({

```

To make these tests pass you may need to:
1. Ensure `DepositView` renders the countdown with `data-testid="countdown-timer"` (e.g. `<span data-testid="countdown-timer">{mm}:{ss}</span>`).
2. Confirm that the countdown is driven by `setInterval` / `setTimeout` so that `vi.useFakeTimers()` can control it.
3. Verify that `DepositView` stops decrementing `timeLeft` when `status !== "waiting"` (e.g. by clearing the interval in an effect cleanup or when status changes). If the status used for "done" is not `"completed"`, adjust the test’s `status` prop accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +68 to +77
const handleMethod = useCallback(
async (method: SupportedPaymentMethod) => {
setLoading(true);
try {
const result = await createCheckout(method.id, amountUsd);
setCheckout(result);
setStatus("waiting");
setStep("deposit");
} catch {
// Stay on method step
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): Errors from createCheckout are swallowed, leaving the user without feedback if checkout creation fails.

The empty catch in handleMethod means createCheckout failures (e.g. network/server errors) are completely silent—the user just stays on the method step with no feedback. Consider surfacing an error (local error state or toast), and ensure loading is cleared with an obvious way to retry so failures aren’t invisible.

Comment on lines +40 to +42
function setNum(key: keyof FeaturesConfig, value: string) {
const n = Number.parseInt(value, 10);
if (!Number.isNaN(n)) setForm((prev) => ({ ...prev, [key]: n }));
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Numeric onboarding fields cannot be cleared back to an empty/zero state by deleting the input, which may be surprising UX.

In setNum, clearing the input (value === "") leads to Number.parseInt returning NaN, so the state isn’t updated and the previous value remains. Consider handling "" explicitly (e.g., map it to 0 or null), or allow setting the state even when the parsed value is NaN and normalize the value on submit, so users can truly clear the field.

Suggested implementation:

  function setStr(key: keyof FeaturesConfig, value: string) {
    setForm((prev) => ({ ...prev, [key]: value || null }));
  }

  function setNum(key: keyof FeaturesConfig, value: string) {
    if (value === "") {
      // Allow clearing numeric fields by mapping an empty input to null
      setForm((prev) => ({ ...prev, [key]: null }));
      return;
    }

    const n = Number.parseInt(value, 10);
    if (!Number.isNaN(n)) {
      setForm((prev) => ({ ...prev, [key]: n }));
    }
  }

import { useState } from "react";
  1. Ensure the corresponding numeric fields in FeaturesConfig are typed as number | null (or a compatible union) to accept the null value used when clearing input.
  2. If there is a submit/normalization layer that expects a different cleared value (e.g., 0 instead of null), adjust the empty-string mapping in setNum accordingly.

Comment on lines +82 to +86
it("calls onSelect when a method is clicked", async () => {
const onSelect = vi.fn();
render(<PaymentMethodPicker methods={METHODS} amountUsd={25} onSelect={onSelect} />);
await userEvent.click(screen.getByText("Bitcoin"));
expect(onSelect).toHaveBeenCalledWith(METHODS[0]);
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider testing the optional onBack behavior when provided

A focused test that renders PaymentMethodPicker with onBack={vi.fn()}, clicks the ← Back button, and asserts the callback is called would help ensure the back navigation remains wired correctly in the multi-step flow.

Comment on lines +19 to +28
describe("DepositView", () => {
it("shows deposit address and amount", () => {
render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
expect(screen.getByText(/32\.24 TRX/)).toBeInTheDocument();
expect(screen.getByText(/THwbQb1s/)).toBeInTheDocument();
});

it("shows waiting status", () => {
render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
expect(screen.getByText(/waiting for payment/i)).toBeInTheDocument();
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Add coverage for non-waiting statuses and back navigation in DepositView

Current tests only cover the waiting/QR/copy flow. Please also add tests for status="partial", "expired", and "failed" to verify the expected text and indicator styles, and a test that the &larr; Back button triggers the onBack callback. This will cover all user-visible states and navigation behavior for the deposit step.

Suggested implementation:

describe("DepositView", () => {
  it("shows deposit address and amount", () => {
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
    expect(screen.getByText(/32\.24 TRX/)).toBeInTheDocument();
    expect(screen.getByText(/THwbQb1s/)).toBeInTheDocument();
  });

  it("shows waiting status", () => {
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
    expect(screen.getByText(/waiting for payment/i)).toBeInTheDocument();
  });

  it("shows partial status", () => {
    render(<DepositView checkout={CHECKOUT} status="partial" onBack={vi.fn()} />);
    expect(screen.getByText(/payment partially received/i)).toBeInTheDocument();
  });

  it("shows expired status", () => {
    render(<DepositView checkout={CHECKOUT} status="expired" onBack={vi.fn()} />);
    expect(screen.getByText(/payment request expired/i)).toBeInTheDocument();
  });

  it("shows failed status", () => {
    render(<DepositView checkout={CHECKOUT} status="failed" onBack={vi.fn()} />);
    expect(screen.getByText(/payment failed/i)).toBeInTheDocument();
  });

  it("calls onBack when back button is clicked", () => {
    const handleBack = vi.fn();
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={handleBack} />);

    const backButton = screen.getByRole("button", { name: /\s*back/i });
    fireEvent.click(backButton);

    expect(handleBack).toHaveBeenCalledTimes(1);
  });

The new tests assume:

  1. The UI strings are:
    • "payment partially received" for status="partial",
    • "payment request expired" for status="expired",
    • "payment failed" for status="failed".
      If DepositView uses different copy, adjust the regexes in getByText to match the actual text.
  2. The back button is rendered as a <button> with accessible name containing "← Back". If the arrow or casing differ, tweak the name matcher (e.g. /back/i), or use getByText/getByRole with the correct label.
  3. If you expose status-specific indicator styles via test IDs or class names (e.g. data-testid="status-indicator"), you can extend each status test to assert the correct indicator styling (e.g. expect(indicator).toHaveClass("status-partial")).

Comment on lines +26 to +28
it("shows waiting status", () => {
render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
expect(screen.getByText(/waiting for payment/i)).toBeInTheDocument();
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider testing the countdown timer behavior and its stopping conditions

Because DepositView keeps a timeLeft countdown while status === "waiting", please add a fake-timer test that:

  • Advances timers and asserts the MM:SS display updates correctly.
  • Asserts that with status !== "waiting", the timer does not keep decrementing.
    This will help catch regressions in countdown logic and interval cleanup.

Suggested implementation:

  it("shows deposit address and amount", () => {
    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);
    expect(screen.getByText(/32\.24 TRX/)).toBeInTheDocument();
    expect(screen.getByText(/THwbQb1s/)).toBeInTheDocument();
  });

  it("updates countdown timer while waiting", () => {
    vi.useFakeTimers();

    render(<DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />);

    const getTimeLeft = () => screen.getByTestId("countdown-timer").textContent;

    const initialTime = getTimeLeft();

    vi.advanceTimersByTime(5_000);

    expect(getTimeLeft()).not.toBe(initialTime);

    vi.useRealTimers();
  });

  it("stops countdown timer when status is no longer waiting", () => {
    vi.useFakeTimers();

    const { rerender } = render(
      <DepositView checkout={CHECKOUT} status="waiting" onBack={vi.fn()} />,
    );

    const getTimeLeft = () => screen.getByTestId("countdown-timer").textContent;

    vi.advanceTimersByTime(5_000);
    const timeAfterWaiting = getTimeLeft();

    rerender(<DepositView checkout={CHECKOUT} status="completed" onBack={vi.fn()} />);

    vi.advanceTimersByTime(5_000);

    expect(getTimeLeft()).toBe(timeAfterWaiting);

    vi.useRealTimers();
  });


vi.mock("qrcode.react", () => ({

To make these tests pass you may need to:

  1. Ensure DepositView renders the countdown with data-testid="countdown-timer" (e.g. <span data-testid="countdown-timer">{mm}:{ss}</span>).
  2. Confirm that the countdown is driven by setInterval / setTimeout so that vi.useFakeTimers() can control it.
  3. Verify that DepositView stops decrementing timeLeft when status !== "waiting" (e.g. by clearing the interval in an effect cleanup or when status changes). If the status used for "done" is not "completed", adjust the test’s status prop accordingly.

@github-actions
Copy link

@claude Please fix all issues raised by the reviewers on this PR.

Feedback available from: coderabbitai[bot]

Steps:

  1. Read every comment from coderabbitai[bot] on this PR
  2. Fix every issue they flagged — bugs, security concerns, TypeScript issues, missing error handling, test gaps
  3. Commit and push fixes to this branch
  4. Remove the auto-fixing label from this PR

Do not close or delete the PR. Do not skip issues. If a reviewer flags something, fix it or leave a clear explanation as a PR comment.
5. Run gh pr merge --auto --squash --repo wopr-network/platform-ui-core 62 to queue it for merge once CI passes.

@greptile-apps
Copy link

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR replaces the horizontal-tab crypto checkout with a 4-step state machine (amount → method → deposit → confirming) that scales to 25+ payment methods, adding AmountSelector, PaymentMethodPicker, DepositView, ConfirmationTracker, and CryptoCheckout components, plus a new admin product configuration page (Brand/Nav/Features/Fleet/Billing tabs). The old BuyCryptoCreditPanel is preserved as a re-export for backward compatibility.

Key issues found:

  • Silent checkout failurehandleMethod in crypto-checkout.tsx swallows errors from createCheckout with no user feedback; users see the loading spinner vanish with no error message and no way to retry knowingly.
  • Unexplained BIP32/BIP39 production dependencies@scure/bip32, @scure/bip39, and @noble/hashes@2.x are added to dependencies in package.json but are not imported anywhere in the new billing components. HD wallet key-derivation libraries have no business being in a UI checkout package; please confirm whether these were accidentally committed.
  • Hardcoded 30-minute countdownDepositView initializes timeLeft to 30 * 60 unconditionally, ignoring any actual expiry returned by the backend in CheckoutResult. If charges expire in a different window, users will see a misleading timer.
  • confirmationsRequired not reset on flow restarthandleReset clears confirmations but not confirmationsRequired, causing stale progress-bar data to briefly flash on the next purchase.
  • bg-white hardcoded on QR container — Minor convention violation against the dark-mode-first rule; should carry a comment documenting the intentional override since white backgrounds are genuinely required for QR scanners.
  • Unused amountUsd propPaymentMethodPicker accepts amountUsd but ignores it (aliased _amountUsd); if the feature isn't ready, the prop should be removed from the interface until needed.

Confidence Score: 2/5

  • Not safe to merge until the unexplained BIP32/BIP39 production dependencies are addressed and the silent checkout-creation failure is fixed
  • The presence of HD wallet key-derivation libraries (@scure/bip32, @scure/bip39) as production dependencies without any visible usage is a blocker — they shouldn't ship in a UI library until the intent is clarified. The silent createCheckout error is a user-facing bug that will cause payment confusion. The hardcoded 30-minute countdown is a correctness issue that could lead to missed payments. The remaining issues (stale confirmationsRequired, bg-white, unused prop) are minor but add up.
  • package-lock.json / package.json (BIP32/BIP39 deps), src/components/billing/crypto-checkout.tsx (silent error, stale reset), src/components/billing/deposit-view.tsx (hardcoded countdown)

Important Files Changed

Filename Overview
src/components/billing/crypto-checkout.tsx Core 4-step state machine; has a silent error swallow on checkout creation failure and confirmationsRequired not reset on flow restart
src/components/billing/deposit-view.tsx QR + countdown view; countdown timer is hardcoded to 30 minutes instead of using the actual charge expiry, and uses hardcoded bg-white against the dark-mode-first convention
src/components/billing/amount-selector.tsx Clean preset + custom amount selector with proper validation; no issues found
src/components/billing/payment-method-picker.tsx Search + filter pill picker with good useMemo optimization; amountUsd prop accepted but intentionally unused, which pollutes the public API
src/components/billing/confirmation-tracker.tsx Progress bar + 3-step timeline component; clean implementation, confirmationsRequired stale state is a parent concern
src/components/billing/buy-crypto-credits-panel.tsx Trivial backward-compat re-export of CryptoCheckout as BuyCryptoCreditPanel; no issues
package-lock.json Adds @scure/bip32, @scure/bip39, and @noble/hashes as production dependencies — HD wallet key-derivation libraries that are not visibly used by any new component; likely an accidental inclusion
src/app/admin/products/page.tsx New admin product config page with tabbed Brand/Nav/Features/Fleet/Billing forms; clean structure with proper null-safety defaults for optional sections
src/components/admin/products/billing-form.tsx Admin billing config form (Stripe key, credit price tiers, affiliate/dividend rates); well-structured with controlled inputs and toast feedback
src/components/admin/products/nav-editor.tsx Drag-free nav item editor with move up/down, add, remove, and per-item enabled toggle; clean local state management

Sequence Diagram

sequenceDiagram
    participant U as User
    participant CC as CryptoCheckout
    participant AS as AmountSelector
    participant PMP as PaymentMethodPicker
    participant DV as DepositView
    participant CT as ConfirmationTracker
    participant API as /api

    CC->>API: getSupportedPaymentMethods()
    API-->>CC: SupportedPaymentMethod[]

    U->>AS: select $25 → Continue
    AS-->>CC: onSelect(25)
    CC->>PMP: render(methods, amountUsd=25)

    U->>PMP: pick USDT/Tron
    PMP-->>CC: onSelect(method)
    CC->>API: createCheckout(method.id, 25)
    API-->>CC: CheckoutResult (depositAddress, referenceId…)
    CC->>DV: render(checkout, status="waiting")

    loop every 5s
        CC->>API: getChargeStatus(referenceId)
        API-->>CC: {status, amountReceivedCents, confirmations}
        alt full amount received
            CC->>CT: setStep("confirming"), status="confirming"
        else partial received
            CC->>DV: status="partial"
        else credited
            CC->>CT: status="credited", clearInterval
        else expired/failed
            CC->>DV: status="expired"|"failed", clearInterval
        end
    end

    U->>CT: "Done — buy more credits"
    CT-->>CC: handleReset()
    CC->>AS: setStep("amount")
Loading

Comments Outside Diff (1)

  1. package-lock.json, line 20-22 (link)

    P1 Unexplained BIP32/BIP39 production dependencies

    @noble/hashes, @scure/bip32, and @scure/bip39 are added as production dependencies (not devDependencies). These are HD wallet key-derivation libraries. None of the new billing components (amount-selector, deposit-view, payment-method-picker, confirmation-tracker, crypto-checkout) import or use them.

    A standard crypto deposit-address checkout flow gets the address from the backend via createCheckout — there is no need for client-side HD wallet derivation in a UI library.

    Concerns:

    1. Bundle size@scure/bip32 + @scure/bip39 + @noble/hashes@2.x add meaningful bytes to the production bundle.
    2. Security — If a future developer accidentally exposes seed phrases or private keys through these libraries in a client-side context, it would be catastrophic.
    3. Accidental addition — These may have been pulled in during an exploratory spike and left in package.json unintentionally.

    Please confirm whether these packages are actually used anywhere, and if not, remove them from dependencies.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: package-lock.json
    Line: 20-22
    
    Comment:
    **Unexplained BIP32/BIP39 production dependencies**
    
    `@noble/hashes`, `@scure/bip32`, and `@scure/bip39` are added as production `dependencies` (not `devDependencies`). These are HD wallet key-derivation libraries. None of the new billing components (`amount-selector`, `deposit-view`, `payment-method-picker`, `confirmation-tracker`, `crypto-checkout`) import or use them.
    
    A standard crypto deposit-address checkout flow gets the address from the backend via `createCheckout` — there is no need for client-side HD wallet derivation in a UI library.
    
    Concerns:
    1. **Bundle size**`@scure/bip32` + `@scure/bip39` + `@noble/hashes@2.x` add meaningful bytes to the production bundle.
    2. **Security** — If a future developer accidentally exposes seed phrases or private keys through these libraries in a client-side context, it would be catastrophic.
    3. **Accidental addition** — These may have been pulled in during an exploratory spike and left in `package.json` unintentionally.
    
    Please confirm whether these packages are actually used anywhere, and if not, remove them from `dependencies`.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/components/billing/crypto-checkout.tsx
Line: 76-78

Comment:
**Silent failure on checkout creation**

When `createCheckout` throws, the catch block is completely silent — the loading spinner disappears but no error is surfaced to the user. They'll see the payment picker remain unchanged with no indication of what went wrong, and may assume the app froze or the click didn't register.

```suggestion
      } catch (err) {
        // Stay on method step — surface the error to the user
        console.error("Checkout creation failed", err);
        // TODO: surface an error toast or inline error state here
      }
```

At minimum, add a toast (`toast.error(...)`) or an inline error state so the user knows the operation failed and can try again.

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

---

This is a comment left during a code review.
Path: src/components/billing/crypto-checkout.tsx
Line: 85-91

Comment:
**`confirmationsRequired` not reset on flow restart**

`handleReset` zeroes out `confirmations` but never resets `confirmationsRequired`. If the user completes a purchase (e.g. required = 20) and then clicks "Done — buy more credits" to start over, the progress bar and confirmation counter will briefly display stale `confirmationsRequired` data from the previous transaction until the new polling cycle updates it.

```suggestion
  const handleReset = useCallback(() => {
    setStep("amount");
    setCheckout(null);
    setStatus("waiting");
    setAmountUsd(0);
    setConfirmations(0);
    setConfirmationsRequired(0);
  }, []);
```

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

---

This is a comment left during a code review.
Path: src/components/billing/deposit-view.tsx
Line: 17

Comment:
**Countdown hardcoded to 30 minutes — not tied to actual charge expiry**

`timeLeft` is initialized to `30 * 60` unconditionally regardless of the actual expiry the Payram/backend returns in `checkout`. If a charge actually expires in 15 minutes (or 60), the displayed countdown will be misleading and could cause user confusion or payment failures.

The `CheckoutResult` type from `@/lib/api` should include an expiry timestamp (e.g. `expiresAt: number | string`). Use that to derive the initial `timeLeft`:

```suggestion
  const [timeLeft, setTimeLeft] = useState(() => {
    if (checkout.expiresAt) {
      const msLeft = new Date(checkout.expiresAt).getTime() - Date.now();
      return Math.max(0, Math.floor(msLeft / 1000));
    }
    return 30 * 60; // fallback
  });
```

If `CheckoutResult` doesn't yet expose `expiresAt`, that field should be added to the API type and backend response.

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

---

This is a comment left during a code review.
Path: src/components/billing/deposit-view.tsx
Line: 48-50

Comment:
**Hardcoded `bg-white` violates dark-mode-first convention**

The WOPR codebase uses dark-mode-first design with semantic tokens (`bg-background`, `text-foreground`). Using `bg-white` here will render an abrupt white box in dark-themed deployments.

That said, QR codes require sufficient contrast to scan reliably. A `bg-white` wrapper around the QR is functionally correct — the right fix is to keep the white background but scope it to the QR element only while acknowledging the override:

```suggestion
      <div className="mx-auto w-fit rounded-lg bg-white p-3" aria-hidden="true">
        {/* bg-white is intentional: QR code scanners require light background */}
        <QRCodeSVG value={checkout.depositAddress} size={140} />
      </div>
```

Add a comment to document the intentional deviation so future linters or reviewers don't flag it accidentally.

**Rule Used:** WOPR codebase conventions:
- Repository pattern is... ([source](https://app.greptile.com/review/custom-context?memory=103fd9f6-56fa-4ef7-b9bf-dc0cb285f062))

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

---

This is a comment left during a code review.
Path: package-lock.json
Line: 20-22

Comment:
**Unexplained BIP32/BIP39 production dependencies**

`@noble/hashes`, `@scure/bip32`, and `@scure/bip39` are added as production `dependencies` (not `devDependencies`). These are HD wallet key-derivation libraries. None of the new billing components (`amount-selector`, `deposit-view`, `payment-method-picker`, `confirmation-tracker`, `crypto-checkout`) import or use them.

A standard crypto deposit-address checkout flow gets the address from the backend via `createCheckout` — there is no need for client-side HD wallet derivation in a UI library.

Concerns:
1. **Bundle size**`@scure/bip32` + `@scure/bip39` + `@noble/hashes@2.x` add meaningful bytes to the production bundle.
2. **Security** — If a future developer accidentally exposes seed phrases or private keys through these libraries in a client-side context, it would be catastrophic.
3. **Accidental addition** — These may have been pulled in during an exploratory spike and left in `package.json` unintentionally.

Please confirm whether these packages are actually used anywhere, and if not, remove them from `dependencies`.

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

---

This is a comment left during a code review.
Path: src/components/billing/payment-method-picker.tsx
Line: 29-33

Comment:
**`amountUsd` prop accepted but intentionally unused**

The `amountUsd` prop is destructured as `_amountUsd` to suppress lint warnings, meaning it's accepted in the public API but has no effect. This silently discards data that could meaningfully improve UX (e.g. showing "≈ 0.00042 BTC" next to each payment method in the list).

If the feature isn't ready, consider either:
- Removing the prop from `PaymentMethodPickerProps` and the parent until it's needed, or
- Adding a `// TODO: display estimated amount per method` comment to document the intent.

Leaving an unused prop in a public interface adds confusion and unnecessary surface area to the component API.

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

Reviews (1): Last reviewed commit: "feat: CryptoCheckout parent wires 4-step..." | Re-trigger Greptile

Comment on lines +76 to +78
} catch {
// Stay on method step
} finally {
Copy link

Choose a reason for hiding this comment

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

P1 Silent failure on checkout creation

When createCheckout throws, the catch block is completely silent — the loading spinner disappears but no error is surfaced to the user. They'll see the payment picker remain unchanged with no indication of what went wrong, and may assume the app froze or the click didn't register.

Suggested change
} catch {
// Stay on method step
} finally {
} catch (err) {
// Stay on method step — surface the error to the user
console.error("Checkout creation failed", err);
// TODO: surface an error toast or inline error state here
}

At minimum, add a toast (toast.error(...)) or an inline error state so the user knows the operation failed and can try again.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/billing/crypto-checkout.tsx
Line: 76-78

Comment:
**Silent failure on checkout creation**

When `createCheckout` throws, the catch block is completely silent — the loading spinner disappears but no error is surfaced to the user. They'll see the payment picker remain unchanged with no indication of what went wrong, and may assume the app froze or the click didn't register.

```suggestion
      } catch (err) {
        // Stay on method step — surface the error to the user
        console.error("Checkout creation failed", err);
        // TODO: surface an error toast or inline error state here
      }
```

At minimum, add a toast (`toast.error(...)`) or an inline error state so the user knows the operation failed and can try again.

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

Comment on lines +85 to +91
const handleReset = useCallback(() => {
setStep("amount");
setCheckout(null);
setStatus("waiting");
setAmountUsd(0);
setConfirmations(0);
}, []);
Copy link

Choose a reason for hiding this comment

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

P2 confirmationsRequired not reset on flow restart

handleReset zeroes out confirmations but never resets confirmationsRequired. If the user completes a purchase (e.g. required = 20) and then clicks "Done — buy more credits" to start over, the progress bar and confirmation counter will briefly display stale confirmationsRequired data from the previous transaction until the new polling cycle updates it.

Suggested change
const handleReset = useCallback(() => {
setStep("amount");
setCheckout(null);
setStatus("waiting");
setAmountUsd(0);
setConfirmations(0);
}, []);
const handleReset = useCallback(() => {
setStep("amount");
setCheckout(null);
setStatus("waiting");
setAmountUsd(0);
setConfirmations(0);
setConfirmationsRequired(0);
}, []);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/billing/crypto-checkout.tsx
Line: 85-91

Comment:
**`confirmationsRequired` not reset on flow restart**

`handleReset` zeroes out `confirmations` but never resets `confirmationsRequired`. If the user completes a purchase (e.g. required = 20) and then clicks "Done — buy more credits" to start over, the progress bar and confirmation counter will briefly display stale `confirmationsRequired` data from the previous transaction until the new polling cycle updates it.

```suggestion
  const handleReset = useCallback(() => {
    setStep("amount");
    setCheckout(null);
    setStatus("waiting");
    setAmountUsd(0);
    setConfirmations(0);
    setConfirmationsRequired(0);
  }, []);
```

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


export function DepositView({ checkout, status, onBack }: DepositViewProps) {
const [copied, setCopied] = useState(false);
const [timeLeft, setTimeLeft] = useState(30 * 60);
Copy link

Choose a reason for hiding this comment

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

P1 Countdown hardcoded to 30 minutes — not tied to actual charge expiry

timeLeft is initialized to 30 * 60 unconditionally regardless of the actual expiry the Payram/backend returns in checkout. If a charge actually expires in 15 minutes (or 60), the displayed countdown will be misleading and could cause user confusion or payment failures.

The CheckoutResult type from @/lib/api should include an expiry timestamp (e.g. expiresAt: number | string). Use that to derive the initial timeLeft:

Suggested change
const [timeLeft, setTimeLeft] = useState(30 * 60);
const [timeLeft, setTimeLeft] = useState(() => {
if (checkout.expiresAt) {
const msLeft = new Date(checkout.expiresAt).getTime() - Date.now();
return Math.max(0, Math.floor(msLeft / 1000));
}
return 30 * 60; // fallback
});

If CheckoutResult doesn't yet expose expiresAt, that field should be added to the API type and backend response.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/billing/deposit-view.tsx
Line: 17

Comment:
**Countdown hardcoded to 30 minutes — not tied to actual charge expiry**

`timeLeft` is initialized to `30 * 60` unconditionally regardless of the actual expiry the Payram/backend returns in `checkout`. If a charge actually expires in 15 minutes (or 60), the displayed countdown will be misleading and could cause user confusion or payment failures.

The `CheckoutResult` type from `@/lib/api` should include an expiry timestamp (e.g. `expiresAt: number | string`). Use that to derive the initial `timeLeft`:

```suggestion
  const [timeLeft, setTimeLeft] = useState(() => {
    if (checkout.expiresAt) {
      const msLeft = new Date(checkout.expiresAt).getTime() - Date.now();
      return Math.max(0, Math.floor(msLeft / 1000));
    }
    return 30 * 60; // fallback
  });
```

If `CheckoutResult` doesn't yet expose `expiresAt`, that field should be added to the API type and backend response.

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

Comment on lines +48 to +50
<div className="mx-auto w-fit rounded-lg bg-white p-3" aria-hidden="true">
<QRCodeSVG value={checkout.depositAddress} size={140} />
</div>
Copy link

Choose a reason for hiding this comment

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

P2 Hardcoded bg-white violates dark-mode-first convention

The WOPR codebase uses dark-mode-first design with semantic tokens (bg-background, text-foreground). Using bg-white here will render an abrupt white box in dark-themed deployments.

That said, QR codes require sufficient contrast to scan reliably. A bg-white wrapper around the QR is functionally correct — the right fix is to keep the white background but scope it to the QR element only while acknowledging the override:

Suggested change
<div className="mx-auto w-fit rounded-lg bg-white p-3" aria-hidden="true">
<QRCodeSVG value={checkout.depositAddress} size={140} />
</div>
<div className="mx-auto w-fit rounded-lg bg-white p-3" aria-hidden="true">
{/* bg-white is intentional: QR code scanners require light background */}
<QRCodeSVG value={checkout.depositAddress} size={140} />
</div>

Add a comment to document the intentional deviation so future linters or reviewers don't flag it accidentally.

Rule Used: WOPR codebase conventions:

  • Repository pattern is... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/billing/deposit-view.tsx
Line: 48-50

Comment:
**Hardcoded `bg-white` violates dark-mode-first convention**

The WOPR codebase uses dark-mode-first design with semantic tokens (`bg-background`, `text-foreground`). Using `bg-white` here will render an abrupt white box in dark-themed deployments.

That said, QR codes require sufficient contrast to scan reliably. A `bg-white` wrapper around the QR is functionally correct — the right fix is to keep the white background but scope it to the QR element only while acknowledging the override:

```suggestion
      <div className="mx-auto w-fit rounded-lg bg-white p-3" aria-hidden="true">
        {/* bg-white is intentional: QR code scanners require light background */}
        <QRCodeSVG value={checkout.depositAddress} size={140} />
      </div>
```

Add a comment to document the intentional deviation so future linters or reviewers don't flag it accidentally.

**Rule Used:** WOPR codebase conventions:
- Repository pattern is... ([source](https://app.greptile.com/review/custom-context?memory=103fd9f6-56fa-4ef7-b9bf-dc0cb285f062))

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +29 to +33
methods,
amountUsd: _amountUsd,
onSelect,
onBack,
}: PaymentMethodPickerProps) {
Copy link

Choose a reason for hiding this comment

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

P2 amountUsd prop accepted but intentionally unused

The amountUsd prop is destructured as _amountUsd to suppress lint warnings, meaning it's accepted in the public API but has no effect. This silently discards data that could meaningfully improve UX (e.g. showing "≈ 0.00042 BTC" next to each payment method in the list).

If the feature isn't ready, consider either:

  • Removing the prop from PaymentMethodPickerProps and the parent until it's needed, or
  • Adding a // TODO: display estimated amount per method comment to document the intent.

Leaving an unused prop in a public interface adds confusion and unnecessary surface area to the component API.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/billing/payment-method-picker.tsx
Line: 29-33

Comment:
**`amountUsd` prop accepted but intentionally unused**

The `amountUsd` prop is destructured as `_amountUsd` to suppress lint warnings, meaning it's accepted in the public API but has no effect. This silently discards data that could meaningfully improve UX (e.g. showing "≈ 0.00042 BTC" next to each payment method in the list).

If the feature isn't ready, consider either:
- Removing the prop from `PaymentMethodPickerProps` and the parent until it's needed, or
- Adding a `// TODO: display estimated amount per method` comment to document the intent.

Leaving an unused prop in a public interface adds confusion and unnecessary surface area to the component API.

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

@github-actions
Copy link

@claude Please fix all issues raised by the reviewers on this PR.

Feedback available from: greptile-apps[bot] and coderabbitai[bot]

Steps:

  1. Read every comment from greptile-apps[bot] and coderabbitai[bot] on this PR
  2. Fix every issue they flagged — bugs, security concerns, TypeScript issues, missing error handling, test gaps
  3. Commit and push fixes to this branch
  4. Remove the auto-fixing label from this PR

Do not close or delete the PR. Do not skip issues. If a reviewer flags something, fix it or leave a clear explanation as a PR comment.
5. Run gh pr merge --auto --squash --repo wopr-network/platform-ui-core 62 to queue it for merge once CI passes.

…colors

- Remove amountUsd from PaymentMethodPicker (unused, confusing API surface)
- Replace bg-white on QR container with bg-background + semantic CSS vars
- QR uses hsl(var(--background/--foreground)) for dark mode compatibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment on lines +19 to +23
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(checkout.depositAddress);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}, [checkout.depositAddress]);
Copy link

Choose a reason for hiding this comment

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

🟢 Low billing/deposit-view.tsx:19

navigator.clipboard.writeText() is not awaited, so setCopied(true) executes immediately even if the clipboard operation fails. Additionally, navigator.clipboard may be undefined in insecure contexts, causing a crash. Consider awaiting the Promise and checking for API availability before calling it.

  const handleCopy = useCallback(() => {
-    navigator.clipboard.writeText(checkout.depositAddress);
-    setCopied(true);
-    setTimeout(() => setCopied(false), 2000);
+    if (!navigator.clipboard) return;
+    navigator.clipboard.writeText(checkout.depositAddress).then(() => {
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    });
   }, [checkout.depositAddress]);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/components/billing/deposit-view.tsx around lines 19-23:

`navigator.clipboard.writeText()` is not awaited, so `setCopied(true)` executes immediately even if the clipboard operation fails. Additionally, `navigator.clipboard` may be `undefined` in insecure contexts, causing a crash. Consider awaiting the Promise and checking for API availability before calling it.

Evidence trail:
src/components/billing/deposit-view.tsx lines 19-23 at REVIEWED_COMMIT: `handleCopy` callback calls `navigator.clipboard.writeText()` without await/then and sets `setCopied(true)` synchronously. No check for `navigator.clipboard` existence. MDN Web Docs confirms Clipboard API requires secure context: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant