Skip to content

feat(billing): add 3dsecure payments ui#1933

Merged
baktun14 merged 9 commits intomainfrom
features/billing-3d-secure
Sep 23, 2025
Merged

feat(billing): add 3dsecure payments ui#1933
baktun14 merged 9 commits intomainfrom
features/billing-3d-secure

Conversation

@baktun14
Copy link
Contributor

@baktun14 baktun14 commented Sep 17, 2025

#1886

Summary by CodeRabbit

  • New Features

    • End-to-end 3D Secure flow (modal/popup, start/close/success/error controls) and wallet-creation flow update.
    • New payment UI: payment method card/list, empty state, error & validation alerts, terms & conditions, trial-start button, and revamped add-card verification UI.
    • Stripe/payment flows updated across onboarding and payment pages.
  • Bug Fixes

    • Disable pay for zero/negative amounts; improved removal/refetch and user-facing error messaging; server-side 3DS ownership checks.
  • Refactor

    • Modularized payment UI and simplified render paths.
  • Tests

    • Expanded coverage for 3DS lifecycle, wallet creation, and edge/error scenarios.

@baktun14 baktun14 requested a review from a team as a code owner September 17, 2025 10:28
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 17, 2025

Walkthrough

Implements end-to-end 3DS flows and wallet-creation mutation across onboarding and payment UIs, adds a reusable use3DSecure hook and ThreeDSecure UI, refactors payment-method UI into reusable components, extends HTTP SDK types/methods for optional 3DS, and expands tests for 3DS and wallet scenarios.

Changes

Cohort / File(s) Summary
Onboarding container & tests
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
Replaces connect-managed-wallet with useCreateManagedWalletMutation, wires use3DSecure and useUser, exposes hasValidatedCard/hasPaymentMethod/threeDSecure to children, changes success/error/remove handling, and adds extensive 3DS and wallet-creation tests and mocks.
Onboarding step (UI + Stripe wiring)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
Adds threeDSecure prop handling, ThreeDSecurePopup rendering, Stripe Elements appearance (theme-aware), conditional form/3DS rendering, and threads hasPaymentMethod.
Payment-methods display refactor
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx, apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx, .../ErrorAlert.tsx, .../TermsAndConditions.tsx, .../TrialStartButton.tsx, .../ValidationWarning.tsx, .../PaymentMethodsDisplay.test.tsx
Breaks monolith into modular components, adds hasPaymentMethod prop, simplifies UI and tests, and delegates error/alert UI to ErrorAlert.
Shared 3DS UI (modal + popup)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx, apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
New ThreeDSecureModal implementing Stripe confirm flow (processing/success/failure) and ThreeDSecurePopup wrapper delegating open/close control to parent.
Shared payment-method primitives
apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx, .../PaymentMethodCard/index.ts, apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx, .../PaymentMethodsList/index.ts
New reusable PaymentMethodCard and PaymentMethodsList with selectable/display modes and index re-exports.
Payment verification card
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
Simplified component to PaymentMethodForm flow; narrows setupIntent typing and refetches payment methods after card addition; removes Elements/theme wrapper.
use3DSecure hook
apps/deploy-web/src/hooks/use3DSecure.ts
New hook managing 3DS state (isOpen, threeDSData), start/close handlers, handle3DSSuccess/handle3DSError, snackbar messaging, and server-side validation mutation wiring.
Payment page integration
apps/deploy-web/src/pages/payment.tsx
Integrates use3DSecure and ThreeDSecurePopup into payment flow (trigger on requiresAction), tightens final-amount validation, and wires updated payment-method list props.
Queries / mutations & hooks
apps/deploy-web/src/queries/usePaymentQueries.ts, apps/deploy-web/src/queries/useManagedWalletQuery.ts, apps/deploy-web/src/hooks/useManagedWallet.ts
confirmPayment returns typed ConfirmPaymentResponse; adds validatePaymentMethodAfter3DS mutation and exposes it; guards cache update on create-managed-wallet when requires3DS; adds explicit typing in managed-wallet hook.
HTTP SDK: managed-wallet & stripe types
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts, packages/http-sdk/src/stripe/stripe.types.ts
Adds ApiThreeDSecureAuth and ApiWalletWithOptional3DS, createWallet returns optional 3DS fields and validatePaymentMethodAfter3DS method; flattens ConfirmPaymentResponse and removes SetupIntentResponse.
Backend Stripe validation & tests
apps/api/src/billing/controllers/stripe/stripe.controller.ts, apps/api/src/billing/services/stripe/stripe.service.ts, apps/api/src/billing/services/stripe/stripe.service.spec.ts
Adds ownership checks in 3DS validation, validatePaymentMethodAfter3DS returns { success: boolean }, logs and status-based handling added, and tests adapted to assert new behaviors and shapes.
User payment cleanup & minor fixes
apps/deploy-web/src/components/user/payment/PaymentMethodsList.tsx (deleted), apps/deploy-web/src/components/user/payment/index.ts, apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx, apps/deploy-web/src/components/user/payment/PaymentForm.tsx
Removes legacy user PaymentMethodsList and its re-export; forces Elements remount via key={clientSecret} in AddPaymentMethodPopup; disables Pay button when amount ≤ 0.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Step as PaymentMethodStep
  participant Container as PaymentMethodContainer
  participant Hook3DS as use3DSecure
  participant API as ManagedWallet API
  participant Q as PaymentQueries

  User->>Step: triggers next/start trial
  Step->>Container: onNext()
  Container->>API: createWallet(userId)
  API-->>Container: { requires3DS?, clientSecret?, paymentIntentId?, paymentMethodId? }
  alt requires3DS
    Container->>Hook3DS: start3DSecure(data)
    Hook3DS-->>Step: isOpen -> render 3DS popup
    Note right of Hook3DS: user authenticates via Stripe
    Hook3DS-->>Container: handle3DSSuccess()
    Container->>Q: validatePaymentMethodAfter3DS(paymentMethodId,paymentIntentId)
    Q-->>Container: { success: true }
    Container->>API: createWallet(userId) (retry)
    API-->>Container: wallet (no 3DS)
    Container-->>Step: onComplete()
  else no 3DS
    Container-->>Step: onComplete()
  end
  alt errors
    Container->>Step: show snackbar / refetch / stop loading
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Page as /payment page
  participant PQ as usePaymentMutations
  participant Hook3DS as use3DSecure
  participant Stripe as Stripe Backend

  User->>Page: Click "Pay"
  Page->>PQ: confirmPayment(params)
  PQ->>Stripe: confirmPayment
  Stripe-->>PQ: { success | requiresAction, clientSecret?, paymentIntentId? }
  alt requiresAction
    Page->>Hook3DS: start3DSecure({clientSecret,paymentIntentId,paymentMethodId})
    Page->>Page: show ThreeDSecurePopup
    Note right of Hook3DS: user authenticates
    Hook3DS-->>Page: onSuccess()
    Page->>Page: show success, reset fields
  else success
    Page->>Page: show success, reset fields
  else failure
    Page->>Page: show error
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • stalniy
  • ygrishajev

Poem

Hoppity hop, I guard the pay,
I nudge the cards through 3DS way.
Secrets shuffled, modals bright,
Wallets born and tests take flight.
Rabbit cheers — payments feel just right! 🐇💳

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat(billing): add 3dsecure payments ui" concisely and accurately summarizes the primary change in this PR — adding 3DSecure UI and related billing/payment flow updates across frontend hooks, components, and supporting API types. It is specific, relevant to the changes in the diff, and follows conventional commit-style formatting so a reviewer scanning history will understand the main intent.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch features/billing-3d-secure

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

@codecov
Copy link

codecov bot commented Sep 17, 2025

Codecov Report

❌ Patch coverage is 15.72770% with 359 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.58%. Comparing base (4b4a020) to head (4da2e27).
⚠️ Report is 8 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...nts/shared/PaymentMethodForm/ThreeDSecureModal.tsx 0.00% 108 Missing and 15 partials ⚠️
apps/deploy-web/src/hooks/use3DSecure.ts 8.00% 41 Missing and 5 partials ⚠️
apps/deploy-web/src/pages/payment.tsx 0.00% 30 Missing ⚠️
...aymentVerificationCard/PaymentVerificationCard.tsx 0.00% 24 Missing and 1 partial ⚠️
.../PaymentMethodContainer/PaymentMethodContainer.tsx 68.00% 22 Missing and 2 partials ⚠️
...ding/steps/PaymentMethodStep/PaymentMethodStep.tsx 0.00% 19 Missing and 2 partials ⚠️
...nts/shared/PaymentMethodCard/PaymentMethodCard.tsx 0.00% 18 Missing and 2 partials ⚠️
...s/shared/PaymentMethodsList/PaymentMethodsList.tsx 0.00% 11 Missing and 2 partials ⚠️
...nts/shared/PaymentMethodForm/ThreeDSecurePopup.tsx 0.00% 9 Missing ⚠️
...rc/billing/controllers/stripe/stripe.controller.ts 0.00% 7 Missing and 1 partial ⚠️
... and 12 more

❌ Your patch status has failed because the patch coverage (14.59%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1933      +/-   ##
==========================================
- Coverage   44.71%   44.58%   -0.14%     
==========================================
  Files         979      990      +11     
  Lines       27596    27969     +373     
  Branches     7118     7320     +202     
==========================================
+ Hits        12339    12469     +130     
+ Misses      14878    14289     -589     
- Partials      379     1211     +832     
Flag Coverage Δ
api 81.12% <46.66%> (-0.05%) ⬇️
deploy-web 22.92% <14.59%> (+0.23%) ⬆️
log-collector 75.35% <ø> (ø)
notifications 87.94% <ø> (ø)
provider-console 81.48% <ø> (ø)
provider-proxy 84.47% <ø> (ø)
Files with missing lines Coverage Δ
.../api/src/billing/services/stripe/stripe.service.ts 88.73% <100.00%> (+0.23%) ⬆️
.../components/user/payment/AddPaymentMethodPopup.tsx 0.00% <ø> (ø)
...ps/deploy-web/src/components/user/payment/index.ts 0.00% <ø> (ø)
...ps/deploy-web/src/queries/useManagedWalletQuery.ts 94.73% <100.00%> (+0.29%) ⬆️
...ymentMethodsDisplay/PaymentMethodsDisplay.test.tsx 0.00% <0.00%> (ø)
...b/src/components/shared/PaymentMethodCard/index.ts 0.00% <0.00%> (ø)
.../src/components/shared/PaymentMethodsList/index.ts 0.00% <0.00%> (ø)
...oy-web/src/components/user/payment/PaymentForm.tsx 0.00% <0.00%> (ø)
apps/deploy-web/src/hooks/useManagedWallet.ts 23.07% <0.00%> (ø)
...teps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx 0.00% <0.00%> (ø)
... and 16 more

... and 198 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsx (1)

316-316: Avoid any[] in TS tests; use a proper type

Guideline: never use any. Type the test data as Partial<PaymentMethod>[] (or a focused pick).

Apply this diff:

+import type { PaymentMethod } from "@akashnetwork/http-sdk/src/stripe/stripe.types";
 ...
-      paymentMethods?: any[];
+      paymentMethods?: Array<Partial<PaymentMethod>>;

If PaymentMethod isn’t accessible here, define a minimal local test type capturing just the used fields.

🧹 Nitpick comments (29)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx (2)

19-22: Generalize the copy (this component likely appears outside “trial” flows).

Remove the trial-specific phrasing or wire these strings through i18n. Current text may be inaccurate in direct-payment contexts.

Suggested neutral copy:

-          <p className="text-sm">
-            You must complete the card validation process before starting your trial. Please wait for the validation to complete or try adding a different card.
-          </p>
+          <p className="text-sm">
+            Please complete card validation before continuing. Wait for validation to finish, or try adding a different card.
+          </p>

16-16: Mark the icon as decorative for screen readers.

Prevents the icon from being redundantly announced.

-          <WarningTriangle className="h-4 w-4" />
+          <WarningTriangle className="h-4 w-4" aria-hidden="true" />
apps/deploy-web/src/components/user/payment/PaymentForm.tsx (1)

128-128: Disable Pay on NaN/invalid amounts (e.g., ".")

parseFloat('.') is NaN; current predicate doesn’t catch it, so the button can enable with an invalid amount. Add a finite-number guard.

-        disabled={!amount || parseFloat(amount) <= 0 || processing || !selectedPaymentMethodId || !!amountError}
+        disabled={
+          !amount ||
+          !Number.isFinite(parseFloat(amount)) ||
+          parseFloat(amount) <= 0 ||
+          processing ||
+          !selectedPaymentMethodId ||
+          !!amountError
+        }
apps/deploy-web/src/queries/useManagedWalletQuery.ts (1)

29-32: Gating looks right; type the cache write and clear stale cache on 3DS path

Minor polish: specify the cached type and null-out any prior wallet to avoid stale UI when requires3DS is true.

-      if (!response.requires3DS) {
-        queryClient.setQueryData([MANAGED_WALLET, response.userId], () => response);
-      }
+      if (!response.requires3DS) {
+        queryClient.setQueryData([MANAGED_WALLET, response.userId] as QueryKey, response);
+      } else {
+        // Ensure no stale wallet remains visible while 3DS is pending
+        queryClient.setQueryData([MANAGED_WALLET, response.userId] as QueryKey, null);
+      }
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx (1)

10-16: External links: open in new tab with noopener (if these are external)

If UrlService.* returns external URLs, add target="_blank" and rel="noopener noreferrer" for security/accessibility. If internal, ignore.

-      <Link href={UrlService.termsOfService()} className="text-primary hover:underline">
+      <Link href={UrlService.termsOfService()} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
         Terms of Service
       </Link>{" "}
       and{" "}
-      <Link href={UrlService.privacyPolicy()} className="text-primary hover:underline">
+      <Link href={UrlService.privacyPolicy()} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
         Privacy Policy
       </Link>
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx (1)

9-9: Mark decorative icon as hidden from AT

Add aria-hidden so screen readers skip the decorative SVG.

-        <CreditCard className="h-16 w-16 text-muted-foreground" />
+        <CreditCard aria-hidden="true" className="h-16 w-16 text-muted-foreground" />
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx (2)

18-18: Decorative icon: add aria-hidden

Avoid announcing non-informational icons.

-        <WarningTriangle className="h-6 w-6" />
+        <WarningTriangle aria-hidden="true" className="h-6 w-6" />

21-23: Hard-coded title reduces reuse; make it a prop with default

Allows reuse beyond trial errors while keeping current UX.

-interface ErrorAlertProps {
-  error?: AppError;
-}
+interface ErrorAlertProps {
+  error?: AppError;
+  title?: string;
+}

-export const ErrorAlert: React.FC<ErrorAlertProps> = ({ error }) => {
+export const ErrorAlert: React.FC<ErrorAlertProps> = ({ error, title = "Failed to Start Trial" }) => {
   if (!error) return null;

   return (
     <Alert className="mx-auto flex max-w-md flex-row items-center gap-2 text-left" variant="destructive">
       <div className="flex-shrink-0 rounded-full bg-card p-3">
         <WarningTriangle className="h-6 w-6" />
       </div>
       <div>
-        <h4 className="font-medium">Failed to Start Trial</h4>
+        <h4 className="font-medium">{title}</h4>
         <p className="text-sm">{extractErrorMessage(error)}</p>
       </div>
     </Alert>
   );
 }
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx (1)

12-14: Prevent unintended form submit and double-activation

Ensure non-submit semantics and disable while loading; add aria-busy for a11y.

-    <LoadingButton onClick={onClick} disabled={disabled} loading={isLoading} className="flex w-full items-center gap-2">
+    <LoadingButton
+      type="button"
+      onClick={onClick}
+      disabled={disabled || isLoading}
+      loading={isLoading}
+      aria-busy={isLoading}
+      className="flex w-full items-center gap-2"
+    >
apps/deploy-web/src/hooks/use3DSecure.ts (1)

62-63: Gate console logs behind a debug flag or use a logger

Excessive console.* in production can be noisy and leak context. Prefer a logger or guard with an env flag.

Apply this pattern:

+const DEBUG_3DS = process.env.NEXT_PUBLIC_DEBUG_3DS === "true";
 ...
-    console.log("3D Secure authentication successful, processing...");
+    DEBUG_3DS && console.log("3D Secure authentication successful, processing...");
 ...
-      console.log("Marking payment method as validated...", {
+      DEBUG_3DS && console.log("Marking payment method as validated...", {
         paymentMethodId: threeDSData.paymentMethodId,
         paymentIntentId: threeDSData.paymentIntentId
       });
 ...
-      console.log("Payment method validation successful");
+      DEBUG_3DS && console.log("Payment method validation successful");
 ...
-      console.error("Failed to validate payment method after 3D Secure:", error);
+      DEBUG_3DS && console.error("Failed to validate payment method after 3D Secure:", error);
 ...
-    console.log("Calling onSuccess callback...");
+    DEBUG_3DS && console.log("Calling onSuccess callback...");
 ...
-      console.error("3D Secure authentication failed:", error);
+      DEBUG_3DS && console.error("3D Secure authentication failed:", error);

Also applies to: 66-76, 78-81, 88-90, 95-96

apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (3)

31-46: Use official Stripe types instead of custom interfaces

Replace hand-rolled StripeError, PaymentIntent, and AuthenticationResult with @stripe/stripe-js types to avoid drift and improve IDE support.

Apply this diff:

 import { Elements, useElements, useStripe } from "@stripe/react-stripe-js";
+import type { PaymentIntentResult, PaymentIntent as StripePaymentIntent, StripeError as StripeJsError } from "@stripe/stripe-js";
 ...
-interface StripeError {
-  message?: string;
-  type?: string;
-  code?: string;
-}
-
-interface PaymentIntent {
-  status: string;
-  id: string;
-}
-
-interface AuthenticationResult {
-  error?: StripeError;
-  paymentIntent?: PaymentIntent;
-}
+// Use Stripe types from @stripe/stripe-js
 ...
-  const processAuthenticationResult = useCallback(
-    (result: AuthenticationResult) => {
+  const processAuthenticationResult = useCallback(
+    (result: PaymentIntentResult) => {
       const { error, paymentIntent } = result;
 ...
-      if (SUCCESSFUL_STATUSES.includes(paymentIntent.status as (typeof SUCCESSFUL_STATUSES)[number])) {
+      if (SUCCESSFUL_STATUSES.includes(paymentIntent.status as (typeof SUCCESSFUL_STATUSES)[number])) {
         handleAuthenticationSuccess(paymentIntent.status);
       } else if (paymentIntent.status === "requires_payment_method") {
 ...
   const performAuthentication = useCallback(async () => {
 ...
-      const result = await stripe.confirmCardPayment(clientSecret);
+      const result: PaymentIntentResult = await stripe.confirmCardPayment(clientSecret);

Also applies to: 84-86, 126-133


52-57: Honor the title prop (currently ignored)

The header hardcodes “Secure Authentication”. Use the provided title for configurability.

Apply this diff:

-  title: _title = "Card Authentication",
+  title = "Card Authentication",
 ...
-          <h3 className="mb-2 text-lg font-semibold">Secure Authentication</h3>
+          <h3 className="mb-2 text-lg font-semibold">{title}</h3>

Also pass title through from the parent as you already do.

Also applies to: 186-195


15-27: Remove or wire unused props: paymentIntentId, onClose

paymentIntentId is never read, and onClose is accepted but unused. Either implement (e.g., a cancel/close action) or drop them to avoid API bloat.

Suggested minimal removal:

-  paymentIntentId?: string;
 ...
-  onClose?: () => void;
 ...
-export const ThreeDSecureModal: React.FC<ThreeDSecureModalProps> = ({
+export const ThreeDSecureModal: React.FC<ThreeDSecureModalProps> = ({
   clientSecret,
-  paymentIntentId,
   onSuccess,
   onError,
   isOpen,
-  onClose: _onClose,
 ...
       <ThreeDSecureForm
         clientSecret={clientSecret}
-        paymentIntentId={paymentIntentId}
         onSuccess={onSuccess}
         onError={onError}

If you intend to keep them, please add a close affordance and/or analytics usage.

Also applies to: 206-218, 251-260

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsx (1)

12-18: Optional: memoize to reduce re-renders

If the parent re-renders often, memoize the list.

Apply this diff:

-export const PaymentMethodsList: React.FC<PaymentMethodsListProps> = ({ paymentMethods, isRemoving, onRemovePaymentMethod }) => (
-  <div className="space-y-4">
-    {paymentMethods.map(method => {
-      return <PaymentMethodCard key={method.id} method={method} isRemoving={isRemoving} onRemove={onRemovePaymentMethod} />;
-    })}
-  </div>
-);
+const PaymentMethodsListBase: React.FC<PaymentMethodsListProps> = ({ paymentMethods, isRemoving, onRemovePaymentMethod }) => (
+  <div className="space-y-4">
+    {paymentMethods.map(method => (
+      <PaymentMethodCard key={method.id} method={method} isRemoving={isRemoving} onRemove={onRemovePaymentMethod} />
+    ))}
+  </div>
+);
+
+export const PaymentMethodsList = React.memo(PaymentMethodsListBase);
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodCard.tsx (2)

21-27: Graceful fallbacks + expiry formatting

Handle missing brand/last4 safely and pad month for readability.

Apply:

-export const PaymentMethodCard: React.FC<PaymentMethodCardProps> = ({ method, isRemoving, onRemove }) => (
-  <Card className="relative">
+export const PaymentMethodCard: React.FC<PaymentMethodCardProps> = ({ method, isRemoving, onRemove }) => {
+  const brandLabel = method.card?.brand ? method.card.brand.toUpperCase() : "CARD";
+  const last4 = method.card?.last4 ?? "••••";
+  const expMonth = method.card?.exp_month != null ? String(method.card.exp_month).padStart(2, "0") : "--";
+  const expYear = method.card?.exp_year ?? "--";
+
+  return (
+  <Card className="relative">
     <CardHeader className="pb-3">
       <div className="flex items-center justify-between">
         <div className="flex items-center gap-3">
           <div className="rounded-full bg-primary/10 p-2">
-            <CreditCard className="h-5 w-5 text-primary" />
+            <CreditCard className="h-5 w-5 text-primary" aria-hidden="true" />
           </div>
           <div>
             <CardTitle className="text-base">
-              {method.card?.brand?.toUpperCase()} •••• {method.card?.last4}
+              {brandLabel} •••• {last4}
             </CardTitle>
             <CardDescription>
-              Expires {method.card?.exp_month}/{method.card?.exp_year}
+              Expires {expMonth}/{expYear}
             </CardDescription>
           </div>
         </div>

30-35: Badge and button a11y

  • Add a title to the success badge.
  • Give “Remove” a descriptive aria-label.
-          {method.validated && (
-            <Badge variant="success" className="flex items-center p-1">
+          {method.validated && (
+            <Badge variant="success" className="flex items-center p-1" title="Validated">
               <CheckCircle className="h-4 w-4" />
             </Badge>
           )}
-          <Button onClick={() => onRemove(method.id)} variant="ghost" size="sm" disabled={isRemoving} className="text-muted-foreground">
+          <Button
+            onClick={() => onRemove(method.id)}
+            aria-label={`Remove ${method.card?.brand ?? "card"} ending ${method.card?.last4 ?? "••••"}`}
+            variant="ghost"
+            size="sm"
+            disabled={isRemoving}
+            className="text-muted-foreground"
+          >
             Remove
           </Button>
apps/deploy-web/src/pages/payment.tsx (5)

45-52: Avoid stale amount in 3DS success callback

onSuccess closes over a potentially stale amount if the user edits it during 3DS. Store the amount at 3DS start and use that on success.

-import React, { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
@@
-  const threeDSecure = use3DSecure({
-    onSuccess: () => {
-      setShowPaymentSuccess({ amount, show: true });
+  const pendingAmountRef = useRef<string>("");
+  const threeDSecure = use3DSecure({
+    onSuccess: () => {
+      setShowPaymentSuccess({ amount: pendingAmountRef.current, show: true });
       setAmount("");
       setCoupon("");
     },
     showSuccessMessage: false
   });

And set it when starting 3DS:

-      if (response && response.requiresAction && response.clientSecret && response.paymentIntentId) {
+      if (response && response.requiresAction && response.clientSecret && response.paymentIntentId) {
+        pendingAmountRef.current = amount;
         threeDSecure.start3DSecure({
           clientSecret: response.clientSecret,
           paymentIntentId: response.paymentIntentId,
           paymentMethodId
         });

86-91: Block payment when amount is invalid

Prevent submission if amountError is set.

-  const handlePayment = async (paymentMethodId: string) => {
-    if (!amount) return;
+  const handlePayment = async (paymentMethodId: string) => {
+    if (!amount || amountError) return;

222-227: Minimum amount check should consider only valid/applicable discounts

Presence of invalid discounts currently bypasses the minimum check.

-    // Only check for minimum amount if no coupon is applied
-    if (!discounts.length && value < MINIMUM_PAYMENT_AMOUNT) {
+    // Only check minimum when there are no valid discounts
+    const hasValidDiscount = discounts.some(d => d.valid);
+    if (!hasValidDiscount && value < MINIMUM_PAYMENT_AMOUNT) {
       setAmountError(`Minimum amount is $${MINIMUM_PAYMENT_AMOUNT}`);
       return false;
     }

354-364: 3DS popup rendering: ensure inner modal is gated by isOpen

This is fixed in the component (see comment there), but double‑check we don’t mount Stripe Elements when closed. Consider also disabling the Pay action while 3DS is open: processing={isConfirmingPayment || threeDSecure.isOpen}.


92-99: Guard missing userId before calling confirmPayment

Page is auth‑gated, but be defensive to avoid noisy server errors if user is still loading.

-    try {
+    try {
+      if (!user?.id) {
+        enqueueSnackbar(<Snackbar title="User not loaded yet. Please try again." iconVariant="error" />, { variant: "error" });
+        return;
+      }
       const response = await confirmPayment({
-        userId: user?.id || "",
+        userId: user.id,
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx (2)

191-199: Avoid “any” in test types

Per guidelines, don’t use any. Use SDK types.

-  function setup(
-    input: {
-      paymentMethods?: any[];
-      setupIntent?: any;
+  function setup(
+    input: {
+      paymentMethods?: import("@akashnetwork/http-sdk/src/stripe/stripe.types").PaymentMethod[];
+      setupIntent?: import("@akashnetwork/http-sdk/src/stripe/stripe.types").SetupIntentResponse;

Also update the test data (Line 178) to match the actual SetupIntentResponse shape (camelCase vs snake_case) used by the app.


231-243: Prefer typed mocks (jest-mock-extended) for complex dependencies

Current jest.fn mocks work, but typed mocks reduce drift and improve DX.

Would you like a follow‑up converting these to mockDeep<…>() with minimal churn?

apps/deploy-web/src/queries/usePaymentQueries.ts (2)

72-87: Add generics to useMutation for stronger typing

Avoids implicit anys and improves inference at call sites.

-  const confirmPayment = useMutation({
-    mutationFn: async ({ userId, paymentMethodId, amount, currency }: ConfirmPaymentParams): Promise<ConfirmPaymentResponse> => {
+  const confirmPayment = useMutation<ConfirmPaymentResponse, Error, ConfirmPaymentParams>({
+    mutationFn: async ({ userId, paymentMethodId, amount, currency }) => {
       return await stripe.confirmPayment({
         userId,
         paymentMethodId,
         amount,
         currency
       });
     },

89-100: Type the 3DS validation mutation and return value

Make the payload/result explicit and keep the return type lean.

-  const validatePaymentMethodAfter3DS = useMutation({
-    mutationFn: async ({ paymentMethodId, paymentIntentId }: ThreeDSecureAuthParams) => {
-      return await stripe.validatePaymentMethodAfter3DS({
+  const validatePaymentMethodAfter3DS = useMutation<{ success: boolean }, Error, ThreeDSecureAuthParams>({
+    mutationFn: async ({ paymentMethodId, paymentIntentId }) => {
+      return await stripe.validatePaymentMethodAfter3DS({
         paymentMethodId,
         paymentIntentId
       });
     },
apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (2)

99-100: Ensure prop compatibility with PaymentVerificationCard.

If you don’t adopt the prop‑type change suggested in PaymentVerificationCard, you’ll need a non‑null assertion or to pass the minimal shape:

-                <PaymentVerificationCard setupIntent={setupIntent} onSuccess={onSuccess} />
+                <PaymentVerificationCard setupIntent={{ clientSecret: setupIntent.clientSecret }} onSuccess={onSuccess} />

115-116: Minor copy tweak for list view.

When showing existing methods, “Payment Methods” is clearer than “Add Payment Method”.

-      <Title>Add Payment Method</Title>
+      <Title>Payment Methods</Title>
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (2)

70-111: Avoid self-referential capture of threeDSecure in its own initializer.

Referencing threeDSecure.start3DSecure inside the callback passed to use3DSecure works but is fragile. Capture start3DSecure after the hook returns and use that in callbacks to avoid TDZ/staleness pitfalls.

-  const threeDSecure = d.use3DSecure({
-    onSuccess: async () => {
+  const threeDSecure = d.use3DSecure({
+    onSuccess: async () => {
       // ...
-        if ("requires3DS" in result && result.requires3DS) {
-          // Start another 3D Secure flow if needed
-          threeDSecure.start3DSecure({
+        if ("requires3DS" in result && result.requires3DS) {
+          const { start3DSecure } = threeDSecure;
+          start3DSecure({
             clientSecret: result.clientSecret || "",
             paymentIntentId: result.paymentIntentId || "",
             paymentMethodId: result.paymentMethodId || ""
           });
           setIsConnectingWallet(false);
           return;
         }

162-173: Optional: nudge users when no payment method on Next.

Early return is fine, but a snackbar (“Add a payment method to continue”) would help users understand why nothing happened.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 959ed00 and 52a5db8.

📒 Files selected for processing (23)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (5 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (4 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodCard.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsx (3 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (2 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1 hunks)
  • apps/deploy-web/src/components/user/payment/PaymentForm.tsx (1 hunks)
  • apps/deploy-web/src/hooks/use3DSecure.ts (1 hunks)
  • apps/deploy-web/src/hooks/useManagedWallet.ts (2 hunks)
  • apps/deploy-web/src/pages/payment.tsx (5 hunks)
  • apps/deploy-web/src/queries/useManagedWalletQuery.ts (1 hunks)
  • apps/deploy-web/src/queries/usePaymentQueries.ts (4 hunks)
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.types.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodCard.tsx
  • apps/deploy-web/src/queries/useManagedWalletQuery.ts
  • packages/http-sdk/src/stripe/stripe.types.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/hooks/use3DSecure.ts
  • apps/deploy-web/src/components/user/payment/PaymentForm.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsx
  • apps/deploy-web/src/hooks/useManagedWallet.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
  • apps/deploy-web/src/pages/payment.tsx
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
  • apps/deploy-web/src/queries/usePaymentQueries.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodCard.tsx
  • apps/deploy-web/src/queries/useManagedWalletQuery.ts
  • packages/http-sdk/src/stripe/stripe.types.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/hooks/use3DSecure.ts
  • apps/deploy-web/src/components/user/payment/PaymentForm.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsx
  • apps/deploy-web/src/hooks/useManagedWallet.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
  • apps/deploy-web/src/pages/payment.tsx
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
  • apps/deploy-web/src/queries/usePaymentQueries.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
apps/{deploy-web,provider-console}/**/*.spec.tsx

📄 CodeRabbit inference engine (.cursor/rules/query-by-in-tests.mdc)

Use queryBy methods instead of getBy methods in test expectations in .spec.tsx files

Files:

  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
🧠 Learnings (1)
📚 Learning: 2025-07-21T08:24:24.269Z
Learnt from: CR
PR: akash-network/console#0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-07-21T08:24:24.269Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` to mock dependencies in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test.

Applied to files:

  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
🧬 Code graph analysis (15)
apps/deploy-web/src/queries/useManagedWalletQuery.ts (1)
apps/deploy-web/src/queries/queryClient.ts (1)
  • queryClient (4-4)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx (2)
apps/deploy-web/src/types/errors.ts (1)
  • AppError (26-26)
apps/deploy-web/src/utils/errorUtils.ts (1)
  • extractErrorMessage (6-28)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx (1)
packages/ui/components/loading-button.tsx (1)
  • LoadingButton (41-41)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (2)
packages/ui/components/spinner.tsx (1)
  • Spinner (10-39)
apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx (1)
  • useServices (27-29)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
apps/deploy-web/src/queries/usePaymentQueries.ts (1)
  • usePaymentMutations (68-130)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsx (1)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodCard.tsx (1)
  • PaymentMethodCard (12-42)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (2)
packages/ui/components/custom/popup.tsx (1)
  • Popup (101-330)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1)
  • ThreeDSecureModal (206-263)
apps/deploy-web/src/hooks/useManagedWallet.ts (1)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (1)
  • ApiManagedWalletOutput (59-59)
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (3)
apps/deploy-web/src/hooks/useUser.ts (1)
  • useUser (7-20)
apps/deploy-web/src/queries/usePaymentQueries.ts (1)
  • usePaymentMethodsQuery (16-26)
apps/deploy-web/src/components/shared/PaymentMethodForm/PaymentMethodForm.tsx (1)
  • PaymentMethodForm (13-83)
apps/deploy-web/src/pages/payment.tsx (3)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
  • use3DSecure (30-126)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)
  • confirmPayment (53-97)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)
  • ThreeDSecurePopup (20-48)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2)
apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts (1)
  • ManagedWalletHttpService (7-66)
packages/http-sdk/src/api-http/api-http.service.ts (1)
  • ApiHttpService (9-29)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (4)
apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx (1)
  • useServices (27-29)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)
  • ThreeDSecurePopup (20-48)
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1)
  • PaymentVerificationCard (16-51)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (1)
  • PaymentMethodsDisplay (21-49)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (4)
apps/deploy-web/src/hooks/useUser.ts (1)
  • useUser (7-20)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (1)
  • createWallet (26-30)
apps/deploy-web/src/utils/errorUtils.ts (1)
  • extractErrorMessage (6-28)
apps/deploy-web/src/types/errors.ts (1)
  • AppError (26-26)
apps/deploy-web/src/queries/usePaymentQueries.ts (5)
packages/http-sdk/src/stripe/stripe.types.ts (3)
  • ConfirmPaymentParams (74-79)
  • ConfirmPaymentResponse (108-113)
  • ThreeDSecureAuthParams (115-118)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (1)
  • validatePaymentMethodAfter3DS (38-48)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)
  • validatePaymentMethodAfter3DS (187-199)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • validatePaymentMethodAfter3DS (50-52)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • validatePaymentMethodAfter3DS (746-779)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (5)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx (1)
  • EmptyPaymentMethods (5-15)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsx (1)
  • PaymentMethodsList (12-18)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx (1)
  • ErrorAlert (12-26)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx (1)
  • TrialStartButton (10-16)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx (1)
  • TermsAndConditions (6-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (9)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (1)

38-48: 3DS validation endpoint wiring looks correct

Posting { paymentMethodId, paymentIntentId } with withCredentials: true and unwrapping via extractApiData aligns with the http-sdk conventions.

apps/deploy-web/src/hooks/use3DSecure.ts (1)

30-37: Hook API and state management look solid

Clear API surface, proper memoization, and correct use of react-query mutation state for isLoading.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsx (1)

63-68: LGTM: test wiring for hasPaymentMethod

The additional prop reflects UI gating and the test adjustments are correct.

Also applies to: 322-323, 351-353

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsx (1)

12-18: LGTM: simple, typed list composition

Typed props, stable keys, and clean mapping.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodCard.tsx (1)

6-10: Confirm type shape for “validated” field

Ensure PaymentMethod includes “validated: boolean” (or make this optional) to avoid type drift with SDK updates.

apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1)

22-27: LGTM: refetch on card add is correct.

Refetching payment methods when a Stripe customer exists keeps UI state consistent. Then delegating to onSuccess is clean.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (1)

64-76: 3DS early-return path looks good.

Clean separation of the 3DS popup flow and wiring of success/error callbacks.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (1)

142-160: LGTM: deletion flow with refetch + snackbar.

Good UX and error handling; refetch after delete keeps list current.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (1)

21-49: LGTM: modular display + start-trial gating.

Clear separation (list/empty/error/CTA/terms) and correct gating of the CTA via hasPaymentMethod.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/deploy-web/src/pages/payment.tsx (1)

167-183: After removal, refresh payment methods to avoid stale UI and selection.

List won’t update until reload; selection may point to a non‑existent card.

Apply this diff:

   const confirmRemovePaymentMethod = async () => {
     if (!cardToDelete) return;
 
     try {
       await removePaymentMethod.mutateAsync(cardToDelete);
-      setSelectedPaymentMethodId(undefined);
+      setSelectedPaymentMethodId(undefined);
+      await refetchPaymentMethods();
       enqueueSnackbar(<Snackbar title="Payment method removed successfully" iconVariant="success" />, { variant: "success" });
     } catch (error: unknown) {
🧹 Nitpick comments (14)
apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx (2)

2-2: Avoid deep type import paths from packages’ src.

Importing types from a package’s internal src can break with packaging changes. Prefer a stable top‑level export (e.g., @akashnetwork/http-sdk) if available.


30-33: Unify empty-state rendering or make it overridable.

This hardcodes copy and style; elsewhere (onboarding) an EmptyPaymentMethods card is used. Expose an optional emptyContent prop and default to null so parents can control UX.

Apply this diff:

 interface PaymentMethodsListProps {
   paymentMethods: PaymentMethod[];
   isRemoving: boolean;
   onRemovePaymentMethod: (paymentMethodId: string) => void;
+  emptyContent?: React.ReactNode;
   // Selection mode props
 export const PaymentMethodsList: React.FC<PaymentMethodsListProps> = ({
   paymentMethods,
   isRemoving,
   onRemovePaymentMethod,
+  emptyContent,
   isSelectable = false,
-  if (paymentMethods.length === 0) {
-    return <p className="text-gray-500">No payment methods added yet.</p>;
-  }
+  if (paymentMethods.length === 0) {
+    return <>{emptyContent ?? null}</>;
+  }
apps/deploy-web/src/pages/payment.tsx (4)

87-99: Re‑validate amount and capture submitted amount before confirming.

Prevents server calls with invalid values and feeds the ref used after 3DS.

Apply this diff:

   const handlePayment = async (paymentMethodId: string) => {
     if (!amount) return;
     if (!selectedPaymentMethodId || !paymentMethods.some(method => method.id === selectedPaymentMethodId)) return;
 
     clearError();
 
     try {
+      const parsed = parseFloat(amount);
+      if (!validateAmount(parsed)) {
+        return;
+      }
+      submittedAmountRef.current = amount;
       const response = await confirmPayment({
         userId: user?.id || "",
         paymentMethodId,
         amount: parseFloat(amount),
         currency: "usd"
       });

74-78: Auto-setting amount to total discount can confuse users.

When discounts load, replacing a blank amount with the discount value may imply “we will charge this.” Consider showing a helper hint instead, or prefill only when a dedicated “Apply discounts” action is taken.


318-319: Fix error title to match context.

This alert surfaces payment/coupon errors on the page, not “loading” issues.

Apply this diff:

-                    <p className="font-medium">Error Loading Payment Information</p>
+                    <p className="font-medium">Payment Error</p>

95-99: Guard against missing userId.

Calling the API with an empty userId can cause opaque server errors; fail fast with a user-facing message.

Apply this diff:

       const response = await confirmPayment({
-        userId: user?.id || "",
+        userId: user?.id ?? (() => { throw new Error("User not loaded. Please re-authenticate.") })(),
         paymentMethodId,
         amount: parseFloat(amount),
         currency: "usd"
       });
apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx (3)

55-59: Provide safe fallbacks for missing card brand/last4.

Prevents rendering blanks like “ •••• ”.

Apply this diff:

-              <div className="text-base font-medium">
-                {method.card?.brand?.toUpperCase()} •••• {method.card?.last4}
-              </div>
+              <div className="text-base font-medium">
+                {(method.card?.brand?.toUpperCase() || "CARD")} •••• {(method.card?.last4 || "••••")}
+              </div>

58-59: Fallback expiry text to avoid “Expires undefined/undefined”.

Apply this diff:

-              <div className="text-sm text-muted-foreground">
-                Expires {method.card?.exp_month}/{method.card?.exp_year}
-              </div>
+              <div className="text-sm text-muted-foreground">
+                Expires {method.card?.exp_month ?? "MM"}/{method.card?.exp_year ?? "YYYY"}
+              </div>
-              <CardDescription>
-                Expires {method.card?.exp_month}/{method.card?.exp_year}
-              </CardDescription>
+              <CardDescription>
+                Expires {method.card?.exp_month ?? "MM"}/{method.card?.exp_year ?? "YYYY"}
+              </CardDescription>

Also applies to: 86-87


42-43: Trim non-essential comments per guidelines.

Commented “Selection/Display mode” lines add noise; code already conveys this.

Apply this diff:

-    // Selection mode - used in payment page
+    {/* */}
-  // Display mode - used in onboarding
+  {/* */}

Also applies to: 72-72

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (2)

2-2: Avoid deep type import paths from packages’ src.

Prefer stable top‑level type exports if available to decouple from package internals.


35-45: Clarify semantics of hasPaymentMethod vs. paymentMethods.length.

List rendering uses length, while Start Trial uses hasPaymentMethod. If hasPaymentMethod implies “validated method only,” consider reflecting that in the list (badge/filter) or compute it here to avoid divergence.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (3)

75-80: Surface a user-facing error when user ID is missing

Currently only logs to console; add a snackbar so users know how to recover.

Apply this diff:

   if (!user?.id) {
     console.error("User ID not available");
     setIsConnectingWallet(false);
-    return;
+    enqueueSnackbar("Unable to identify user. Please sign in and try again.", { variant: "error", autoHideDuration: 5000 });
+    return;
   }

Also applies to: 167-171


67-69: Tidy: simplify hasValidatedCard computation

Array#some already handles empty arrays. Also be explicit about boolean coercion.

Apply this diff:

-  const hasValidatedCard = paymentMethods.length > 0 && paymentMethods.some(method => method.validated);
-  const hasPaymentMethod = paymentMethods.length > 0;
+  const hasPaymentMethod = paymentMethods.length > 0;
+  const hasValidatedCard = paymentMethods.some(method => method.validated === true);

81-103: DRY the wallet creation + 3DS branch

The 3DS handling block is duplicated in onSuccess and handleNext. Extract a small helper to reduce drift.

Example refactor:

+  const connectWalletWith3DS = async (userId: string) => {
+    const result = await createWallet(userId);
+    if ("requires3DS" in result && result.requires3DS) {
+      if (!result.clientSecret || !result.paymentIntentId || !result.paymentMethodId) {
+        setIsConnectingWallet(false);
+        enqueueSnackbar("3D Secure data missing. Please try again.", { variant: "error", autoHideDuration: 5000 });
+        return false;
+      }
+      threeDSecure.start3DSecure({
+        clientSecret: result.clientSecret,
+        paymentIntentId: result.paymentIntentId,
+        paymentMethodId: result.paymentMethodId
+      });
+      setIsConnectingWallet(false);
+      return false;
+    }
+    return true;
+  };

Then call if (await connectWalletWith3DS(user.id)) { setIsConnectingWallet(false); onComplete(); } in both places.

Also applies to: 173-197

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52a5db8 and 018e6c1.

📒 Files selected for processing (11)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (5 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (4 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (2 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodCard/index.ts (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodsList/index.ts (1 hunks)
  • apps/deploy-web/src/components/user/payment/PaymentMethodsList.tsx (0 hunks)
  • apps/deploy-web/src/components/user/payment/index.ts (0 hunks)
  • apps/deploy-web/src/hooks/use3DSecure.ts (1 hunks)
  • apps/deploy-web/src/pages/payment.tsx (6 hunks)
💤 Files with no reviewable changes (2)
  • apps/deploy-web/src/components/user/payment/index.ts
  • apps/deploy-web/src/components/user/payment/PaymentMethodsList.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/deploy-web/src/components/shared/PaymentMethodCard/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/deploy-web/src/hooks/use3DSecure.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/components/shared/PaymentMethodsList/index.ts
  • apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx
  • apps/deploy-web/src/pages/payment.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/components/shared/PaymentMethodsList/index.ts
  • apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx
  • apps/deploy-web/src/pages/payment.tsx
🧬 Code graph analysis (4)
apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx (1)
apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx (1)
  • PaymentMethodCard (20-104)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (4)
apps/deploy-web/src/hooks/useUser.ts (1)
  • useUser (7-20)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (1)
  • createWallet (26-30)
apps/deploy-web/src/utils/errorUtils.ts (1)
  • extractErrorMessage (6-28)
apps/deploy-web/src/types/errors.ts (1)
  • AppError (26-26)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (5)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx (1)
  • EmptyPaymentMethods (5-15)
apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx (1)
  • PaymentMethodsList (20-68)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx (1)
  • ErrorAlert (12-26)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx (1)
  • TrialStartButton (10-16)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx (1)
  • TermsAndConditions (6-19)
apps/deploy-web/src/pages/payment.tsx (4)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
  • use3DSecure (30-125)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (2)
  • confirmPayment (53-97)
  • removePaymentMethod (123-145)
packages/http-sdk/src/stripe/stripe.service.ts (2)
  • confirmPayment (46-48)
  • removePaymentMethod (32-34)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)
  • ThreeDSecurePopup (20-48)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (9)
apps/deploy-web/src/components/shared/PaymentMethodsList/index.ts (1)

1-1: LGTM: clean re-export.

apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx (2)

40-41: Confirm RadioGroup value can be undefined.

If RadioGroup expects a string, passing undefined can flip between controlled/uncontrolled. If the UI lib requires a value, default it to paymentMethods[0].id.


20-68: Solid split between selection and display modes.

apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx (1)

20-104: Overall: clear split, good event handling, no any/TDZ issues.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (1)

31-39: LGTM: nice modularization and reuse of shared list.

apps/deploy-web/src/pages/payment.tsx (1)

94-114: Resolved — backend converts dollars to cents before calling Stripe.

The frontend sends parseFloat(amount) (dollars), and apps/api/src/billing/services/stripe/stripe.service.ts computes finalAmountCents and calls paymentIntents.create({ amount: finalAmountCents, ... }) (see the "$1.00 USD in cents" comment). No change required.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (3)

70-110: 3DS flow wiring and error handling look solid

Nice integration of use3DSecure, clear async flow, and consistent user-facing errors via extractErrorMessage + snackbars.

Please sanity-check with a card that requires 3DS and one that doesn’t to confirm:

  • 3DS opens, success retries wallet creation, and completes.
  • Non-3DS path completes without opening the modal.

Also applies to: 160-198


143-157: Removal flow UX + refetch is correct

Good: confirmation gating, optimistic closure, refetch, and user-visible error handling.


42-51: threeDSecure shape verification — matches hook return

use3DSecure (apps/deploy-web/src/hooks/use3DSecure.ts) returns isOpen, threeDSData (clientSecret, paymentIntentId, paymentMethodId), handle3DSSuccess: () => Promise and handle3DSError: (error: string) => void — types align with PaymentMethodContainer/PaymentMethodStep usage; no change required.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (2)

15-23: Good: kept hooks behind the DI seam (useUser/use3DSecure).

Matches the dependency-injection pattern and improves testability.


147-152: Duplicate onComplete risk addressed.

Setting isConnectingWallet=false before onComplete prevents the effect from firing again. LGTM.

🧹 Nitpick comments (8)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (1)

114-139: Avoid passing empty strings to 3DS; pass only defined fields.

Empty strings can confuse downstream validators. Pass only present IDs.

-      threeDSecure.start3DSecure({
-        clientSecret: clientSecret.trim(),
-        paymentIntentId: paymentIntentId?.trim() || "",
-        paymentMethodId: paymentMethodId?.trim() || ""
-      });
+      threeDSecure.start3DSecure({
+        clientSecret: clientSecret.trim(),
+        ...(paymentIntentId?.trim() ? { paymentIntentId: paymentIntentId.trim() } : {}),
+        ...(paymentMethodId?.trim() ? { paymentMethodId: paymentMethodId.trim() } : {})
+      });
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (1)

18-23: Export a reusable ThreeDS payload type to de-duplicate FE typings.

 export interface ApiWalletWithOptional3DS extends ApiWalletOutput {
   requires3DS?: boolean;
   clientSecret?: string;
   paymentIntentId?: string;
   paymentMethodId?: string;
 }
+
+// Reusable payload for FE contracts (omit the boolean flag).
+export type ThreeDSData = Pick<ApiThreeDSecureAuth, "clientSecret" | "paymentIntentId" | "paymentMethodId">;
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)

33-46: Wire onClose to Popup.

Prop is accepted but unused; connect it so ESC/close can notify parents.

-    <Popup variant="custom" title={title} open={isOpen} enableCloseOnBackdropClick={false} hideCloseButton maxWidth="sm" actions={[]}>
+    <Popup
+      variant="custom"
+      title={title}
+      open={isOpen}
+      onClose={_onClose}
+      enableCloseOnBackdropClick={false}
+      hideCloseButton
+      maxWidth="sm"
+      actions={[]}
+    >
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (5)

28-42: Use Stripe’s official types instead of local re-definitions.

Prevents drift and improves IntelliSense. Replace custom interfaces with @stripe/stripe-js types.

+import type { PaymentIntent, PaymentIntentResult, StripeError } from "@stripe/stripe-js";
@@
-interface StripeError {
-  message?: string;
-  type?: string;
-  code?: string;
-}
-
-interface PaymentIntent {
-  status: string;
-  id: string;
-}
-
-interface AuthenticationResult {
-  error?: StripeError;
-  paymentIntent?: PaymentIntent;
-}
+// Use Stripe types instead of local interfaces.
@@
-  const processAuthenticationResult = useCallback(
-    (result: AuthenticationResult) => {
+  const processAuthenticationResult = useCallback(
+    (result: PaymentIntentResult) => {

Also applies to: 81-83, 2-9


44-54: Clean up component props type.

Omit<ThreeDSecureModalProps, "isOpen" | "onClose"> removes keys that don’t exist. Use the base props type directly.

-const ThreeDSecureForm: React.FC<Omit<ThreeDSecureModalProps, "isOpen" | "onClose">> = ({
+const ThreeDSecureForm: React.FC<ThreeDSecureModalProps> = ({

189-191: Use the provided title prop in the UI.

title is defined but not rendered. Hook it up.

-  title: _title = "Card Authentication",
+  title: _title = "Card Authentication",
@@
-          <h3 className="mb-2 text-lg font-semibold">Secure Authentication</h3>
+          <h3 className="mb-2 text-lg font-semibold">{_title}</h3>

Also applies to: 49-50


70-79: Gate console logging behind a debug flag or use the app logger.

Avoid noisy production logs and potential leakage of error details.

Example:

-      console.log("3D Secure authentication successful, status:", paymentIntentStatus);
+      if (process.env.NEXT_PUBLIC_DEBUG === "true") {
+        console.debug("3DS success:", paymentIntentStatus);
+      }

Repeat similarly for other console.log/error calls.

Also applies to: 85-90, 123-131, 101-106


46-53: Remove unused paymentIntentId plumb-through from ThreeDSecureForm

ThreeDSecureModal accepts and forwards paymentIntentId, but ThreeDSecureForm renames it to _paymentIntentId and never uses it — remove it from the inner component's props and from the JSX invocation to avoid confusion (keep paymentIntentId on ThreeDSecureModal since call sites rely on it).

File: apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx — signature around lines 46–53 and invocation at lines 242–245.

-  paymentIntentId: _paymentIntentId,
   onSuccess,
   onError,
   title: _title = "Card Authentication",
@@
-      <ThreeDSecureForm
+      <ThreeDSecureForm
         clientSecret={clientSecret}
-        paymentIntentId={paymentIntentId}
         onSuccess={onSuccess}
         onError={onError}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 018e6c1 and d3ab046.

📒 Files selected for processing (9)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (5 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (4 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1 hunks)
  • apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx (1 hunks)
  • apps/deploy-web/src/pages/payment.tsx (7 hunks)
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/deploy-web/src/pages/payment.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • packages/http-sdk/src/stripe/stripe.types.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
🧬 Code graph analysis (5)
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (4)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • SetupIntentResponse (66-68)
apps/deploy-web/src/hooks/useUser.ts (1)
  • useUser (7-20)
apps/deploy-web/src/queries/usePaymentQueries.ts (1)
  • usePaymentMethodsQuery (16-26)
apps/deploy-web/src/components/shared/PaymentMethodForm/PaymentMethodForm.tsx (1)
  • PaymentMethodForm (13-83)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (2)
packages/ui/components/custom/popup.tsx (1)
  • Popup (101-330)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1)
  • ThreeDSecureModal (203-255)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2)
apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts (1)
  • ManagedWalletHttpService (7-66)
packages/http-sdk/src/api-http/api-http.service.ts (1)
  • ApiHttpService (9-29)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (2)
packages/ui/components/spinner.tsx (1)
  • Spinner (10-39)
apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx (1)
  • useServices (27-29)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (3)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2)
  • createWallet (26-30)
  • ApiWalletWithOptional3DS (18-23)
apps/deploy-web/src/utils/errorUtils.ts (1)
  • extractErrorMessage (6-28)
apps/deploy-web/src/types/errors.ts (1)
  • AppError (26-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: codecov/project/provider-proxy
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (7)
apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx (1)

27-27: Forcing Elements remount on clientSecret change — good call.

Prevents stale Stripe context when rotating SetupIntents. LGTM.

apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (2)

12-15: Prop type adjustment matches callers.

Optional setupIntent with Pick<SetupIntentResponse,"clientSecret"> aligns with upstream and avoids TS errors. LGTM.


30-41: Loading state is fine.

Early return avoids rendering form without a clientSecret. LGTM.

packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2)

25-31: Preserving optional 3DS fields via generic addWalletEssentials — good.

createWallet now returns the enriched union; callers can branch on requires3DS safely. LGTM.


38-47: New endpoint: validatePaymentMethodAfter3DS — clear shape.

Typed response and withCredentials are appropriate. LGTM.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (1)

25-53: Verified — no breaking consumers found.

Only consumer is OnboardingView which forwards the children props via spread to PaymentMethodStep (OnboardingView.tsx ~line 61). PaymentMethodStep already accepts hasPaymentMethod and threeDSecure (steps/PaymentMethodStep/PaymentMethodStep.tsx); hasValidatedCard is passed but unused. No changes required to call sites or tests.

apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1)

219-238: Confirm intended Stripe appearance: colorSuccess set to brand red.

Success styled as red can read as error. If intentional branding, ignore; otherwise switch to a green token.

-            colorSuccess: "#ff424c"
+            colorSuccess: "#22c55e" /* tailwind green-500 */

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (1)

86-104: Good: remount Elements on clientSecret changes.

key={setupIntent.clientSecret} prevents stale PaymentElement state.

apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1)

10-13: Treat “processing” as non-failure; remove manual timeout to avoid onError/onSuccess races.

Stripe can return status="processing" for async authorizations; that should not trigger failure. The 30s timeout can fire just before Stripe resolves success, causing both callbacks to run.

-const AUTHENTICATION_TIMEOUT = 30_000;
 const SUCCESS_DELAY = 1_500;
-const SUCCESSFUL_STATUSES = ["succeeded", "requires_capture"] as const;
+const SUCCESSFUL_STATUSES = ["succeeded", "requires_capture", "processing"] as const;
-  useEffect(() => {
+  useEffect(() => {
     if (!stripe || !elements || status !== "processing") {
       return;
     }
-
-    const timeout = setTimeout(() => {
-      if (status === "processing") {
-        handleAuthenticationFailure("Authentication timed out. Please try again.");
-      }
-    }, AUTHENTICATION_TIMEOUT);
-
     performAuthentication();

     return () => {
-      clearTimeout(timeout);
       authenticationInProgress.current = false;
     };
   }, [stripe, elements, status, performAuthentication, handleAuthenticationFailure]);
🧹 Nitpick comments (3)
apps/deploy-web/src/pages/payment.tsx (1)

88-96: Validate at submit time to avoid NaN/edge cases slipping through.

If the input is “.” or otherwise parses to NaN, the current checks may proceed. Short-circuit on invalid or failing validateAmount.

-const handlePayment = async (paymentMethodId: string) => {
-  if (!amount) return;
+const handlePayment = async (paymentMethodId: string) => {
+  if (!amount) return;
+  const parsed = parseFloat(amount);
+  if (!Number.isFinite(parsed) || !validateAmount(parsed)) return;
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (2)

185-204: Use the provided title prop; current UI always hardcodes “Secure Authentication”.

The title prop is accepted but ignored. Either use it or drop the prop.

-          <h3 className="mb-2 text-lg font-semibold">Secure Authentication</h3>
+          <h3 className="mb-2 text-lg font-semibold">{_title}</h3>

125-138: Optional: pass Elements instance to confirmCardPayment for consistency.

Not required for SCA-only flows, but passing elements can improve compatibility when PaymentElement is present.

-      const result = await stripe.confirmCardPayment(clientSecret, {
-        payment_method: paymentMethodId
-      });
+      const result = await stripe.confirmCardPayment(
+        clientSecret,
+        { payment_method: paymentMethodId },
+        { elements }
+      );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3ab046 and cc9e9ac.

📒 Files selected for processing (6)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (5 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (4 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1 hunks)
  • apps/deploy-web/src/pages/payment.tsx (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • apps/deploy-web/src/pages/payment.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • apps/deploy-web/src/pages/payment.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
🧬 Code graph analysis (4)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (2)
packages/ui/components/spinner.tsx (1)
  • Spinner (10-39)
apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx (1)
  • useServices (27-29)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (5)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
  • ThreeDSecureData (6-10)
apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx (1)
  • useServices (27-29)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)
  • ThreeDSecurePopup (21-50)
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1)
  • PaymentVerificationCard (17-65)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (1)
  • PaymentMethodsDisplay (21-49)
apps/deploy-web/src/pages/payment.tsx (4)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
  • use3DSecure (30-125)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (2)
  • confirmPayment (53-97)
  • removePaymentMethod (123-145)
packages/http-sdk/src/stripe/stripe.service.ts (2)
  • confirmPayment (46-48)
  • removePaymentMethod (32-34)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)
  • ThreeDSecurePopup (21-50)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (3)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2)
  • createWallet (26-30)
  • ApiWalletWithOptional3DS (18-23)
apps/deploy-web/src/utils/errorUtils.ts (1)
  • extractErrorMessage (6-28)
apps/deploy-web/src/types/errors.ts (1)
  • AppError (26-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (6)
apps/deploy-web/src/pages/payment.tsx (2)

287-291: PaymentMethodsList props wiring looks good.

Passing isRemoving/onRemove/isSelectable aligns with the new list API.


358-371: 3DS popup wiring is correct and guarded.

Rendered only when a clientSecret exists; success/error handlers are correctly delegated.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (1)

65-80: Early return for 3DS popup is clean and avoids double UI.

The guard on both isOpen and threeDSData is appropriate.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (3)

73-111: 3DS success retry flow is solid; avoids duplicate onComplete.

Nice: refetch methods, re-run wallet creation, and clear isConnectingWallet before calling onComplete.


169-187: Post‑removal refresh and UX are correct.

Refetch after removal and surface errors via snackbar.


189-227: Wallet creation + 3DS gating logic looks correct.

Guards user.id, starts 3DS only when payload is valid, and prevents double-complete.

- Add ThreeDSecureModal and ThreeDSecurePopup components
- Implement use3DSecure hook for payment authentication flow
- Update payment page with 3D Secure integration
- Add support for 3D Secure authentication in payment flow
- Refactor PaymentMethodsDisplay components with better organization
- Add new UI components: EmptyPaymentMethods, ErrorAlert, PaymentMethodCard
- Add TermsAndConditions, TrialStartButton, ValidationWarning components
- Update PaymentMethodContainer and PaymentMethodStep with improved UX
- Enhance PaymentVerificationCard with better validation flow
- Update payment queries and managed wallet integration
- Improve payment form user experience
@baktun14 baktun14 force-pushed the features/billing-3d-secure branch from cc9e9ac to 90447eb Compare September 18, 2025 08:21
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/deploy-web/src/pages/payment.tsx (1)

74-79: Align validation with new discount policy: remove discount-driven prefill and “final amount after discount” checks

Per the updated flow, discounts are applied to balance via claim, not to the payment charge. Prefilling amount from discounts and validating “final amount after discount” are misleading.

Apply:

@@
-  useEffect(() => {
-    if (discounts.length > 0 && !amount) {
-      setAmount(getDiscountedAmount().toString());
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [discounts]);
@@
   const validateAmount = (value: number) => {
-    const finalAmount = getFinalAmount(value.toString());
-
     if (value <= 0) {
       setAmountError("Amount must be greater than $0");
       return false;
     }
 
-    if (!discounts.length && value < MINIMUM_PAYMENT_AMOUNT) {
-      setAmountError(`Minimum amount is $${MINIMUM_PAYMENT_AMOUNT}`);
-      return false;
-    }
-
-    if (finalAmount > 0 && finalAmount < 1) {
-      setAmountError("Final amount after discount must be at least $1");
+    if (value < MINIMUM_PAYMENT_AMOUNT) {
+      setAmountError(`Minimum amount is $${MINIMUM_PAYMENT_AMOUNT}`);
       return false;
     }
 
     setAmountError(undefined);
     return true;
   };

Also applies to: 218-236

apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx (1)

352-363: Remove all any usage; tests are also subject to our TS rules

Replace anys with concrete types from the SDK to keep type safety.

-  function setup(
-    input: {
-      paymentMethods?: any[];
-      setupIntent?: any;
-      hasManagedWallet?: boolean;
-      isWalletLoading?: boolean;
-      isConnectingWallet?: boolean;
-      isRemoving?: boolean;
-      onComplete?: jest.Mock;
-      user?: { id: string } | null;
-      managedWalletError?: Error;
-    } = {}
-  ) {
+  function setup(
+    input: {
+      paymentMethods?: Array<Partial<PaymentMethod> & { validated?: boolean }>;
+      setupIntent?: SetupIntentResponse;
+      hasManagedWallet?: boolean;
+      isWalletLoading?: boolean;
+      isConnectingWallet?: boolean;
+      isRemoving?: boolean;
+      onComplete?: jest.Mock;
+      user?: { id: string } | null;
+      managedWalletError?: Error;
+    } = {}
+  ) {

Add at top:

+import type { PaymentMethod, SetupIntentResponse } from "@akashnetwork/http-sdk/src/stripe/stripe.types";
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (1)

39-40: Public API mismatch: onNext is async but typed as () => void

Update the children prop type so consumers (and tests) can await onNext().

-    onNext: () => void;
+    onNext: () => Promise<void>;
♻️ Duplicate comments (1)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx (1)

101-104: onNext is awaited here — make the prop type Promise-aware

Tests await onNext; the component implements it as async. Ensure the prop type is () => Promise<void> (see component file comment).

🧹 Nitpick comments (20)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx (2)

16-16: A11y: mark decorative icon as hidden from screen readers

This icon is purely visual and has adjacent text. Hide it from SR to avoid noise.

-          <WarningTriangle className="h-4 w-4" />
+          <WarningTriangle aria-hidden="true" focusable="false" className="h-4 w-4" />

5-7: i18n: externalize user-facing strings or make them overridable

Hardcoded English strings limit localization/reuse. Consider props with sensible defaults.

 interface ValidationWarningProps {
-  show: boolean;
+  show: boolean;
+  title?: string;
+  message?: string;
 }
 
-export const ValidationWarning: React.FC<ValidationWarningProps> = ({ show }) => {
+export const ValidationWarning: React.FC<ValidationWarningProps> = ({
+  show,
+  title = "Card Validation Required",
+  message = "You must complete the card validation process before starting your trial. Please wait for the validation to complete or try adding a different card.",
+}) => {
   if (!show) return null;
 
   return (
     <Alert className="mx-auto max-w-md text-left" variant="warning">
       <div className="flex items-center gap-2">
         <div className="rounded-full bg-card p-3">
           <WarningTriangle aria-hidden="true" focusable="false" className="h-4 w-4" />
         </div>
         <div>
-          <h4 className="font-medium">Card Validation Required</h4>
+          <h4 className="font-medium">{title}</h4>
           <p className="text-sm">
-            You must complete the card validation process before starting your trial. Please wait for the validation to complete or try adding a different card.
+            {message}
           </p>
         </div>
       </div>
     </Alert>
   );
 };

Also applies to: 9-9, 19-22

apps/api/src/billing/services/stripe/stripe.service.spec.ts (1)

1353-1367: Great: negative test for mismatched PaymentIntent customer.

Add a companion test for mismatched PI.payment_method to lock in the new guard.

Apply this addition near here:

+    it("throws error when payment intent references a different payment method", async () => {
+      const { service } = setup();
+      const mockPaymentIntent = {
+        id: "pi_123",
+        status: "succeeded",
+        customer: "cus_123",
+        payment_method: "pm_other"
+      } as Stripe.PaymentIntent;
+      jest.spyOn(service.paymentIntents, "retrieve").mockResolvedValue(mockPaymentIntent as any);
+      await expect(
+        service.validatePaymentMethodAfter3DS("cus_123", "pm_123", "pi_123")
+      ).rejects.toThrow("Payment intent does not reference the provided payment method");
+    });
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1)

18-28: Refetch on card-added is sound; minor UX note.

The conditional refetch tied to user?.stripeCustomerId is correct. Consider surfacing a tiny success toast inside handleCardAdded to confirm the refresh before onSuccess navigations.

apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (5)

44-55: Unused prop alias paymentIntentId: rename or use to avoid TS unused warnings.

paymentIntentId is destructured as _paymentIntentId but never referenced.

Apply one of:

  • Use it in a debug log to avoid unused warnings.
  • Or remove it from ThreeDSecureForm props and stop passing it (requires touching the parent and Popup).

Minimal change:

-  paymentIntentId: _paymentIntentId,
+  paymentIntentId,
@@
-  const handleAuthenticationSuccess = useCallback(
+  const handleAuthenticationSuccess = useCallback(
     (paymentIntentStatus: string) => {
-      console.log("3D Secure authentication successful, status:", paymentIntentStatus);
+      console.log("3D Secure authentication successful", { paymentIntentId, status: paymentIntentStatus });

28-42: Use Stripe’s official types instead of custom interfaces.

Leverage @stripe/stripe-js types for stronger guarantees and to avoid drift.

+import type { Stripe } from "@stripe/stripe-js";
@@
-interface StripeError {
-  message?: string;
-  type?: string;
-  code?: string;
-}
-
-interface PaymentIntent {
-  status: string;
-  id: string;
-}
-
-interface AuthenticationResult {
-  error?: StripeError;
-  paymentIntent?: PaymentIntent;
-}
+// Prefer Stripe.PaymentIntentResult, Stripe.StripeError, Stripe.PaymentIntent
@@
-  const processAuthenticationResult = useCallback(
-    (result: AuthenticationResult) => {
+  const processAuthenticationResult = useCallback(
+    (result: Stripe.PaymentIntentResult) => {

No behavior change; just typing. Ensure any remaining references use Stripe.PaymentIntent and Stripe.StripeError.

Also applies to: 82-112


71-80: Clear the success delay timer on unmount to avoid firing on unmounted components.

Store the timeout id and clear it in cleanup.

+  const successTimerRef = useRef<number | null>(null);
@@
-      setTimeout(() => {
+      successTimerRef.current = window.setTimeout(() => {
         onSuccess();
-      }, SUCCESS_DELAY);
+      }, SUCCESS_DELAY);
@@
   return () => {
     authenticationInProgress.current = false;
+    if (successTimerRef.current) {
+      clearTimeout(successTimerRef.current);
+      successTimerRef.current = null;
+    }
   };

Also applies to: 146-149


214-215: Memoize stripePromise for stability (minor).

If getStripe() returns a new Promise per call, Elements may re-init more often than needed. Align with other files using useMemo.

-  const stripePromise = stripeService.getStripe();
+  const stripePromise = useMemo(() => stripeService.getStripe(), [stripeService]);

151-196: Console logs: gate or downgrade in production.

Consider routing logs through your logger and gating by env/log level.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (3)

84-112: Show a loader when setupIntent is not yet available.

Currently nothing renders while setupIntent is undefined. Add a fallback to avoid a blank area.

-        {setupIntent?.clientSecret && (
+        {stripePromise ? (
+          setupIntent?.clientSecret ? (
           <ErrorBoundary fallback={<div>Failed to load payment form</div>}>
             {stripePromise ? (
               <Elements
                 key={setupIntent.clientSecret}
                 stripe={stripePromise}
                 options={{
                   clientSecret: setupIntent.clientSecret,
                   appearance: {
                     theme: isDarkMode ? "night" : "stripe",
                     variables: {
                       colorPrimary: "#ff424c",
                       colorSuccess: "#ff424c"
                     }
                   }
                 }}
               >
                 <PaymentVerificationCard setupIntent={setupIntent} onSuccess={onSuccess} />
               </Elements>
-            ) : (
-              <div className="p-4 text-center text-muted-foreground">
-                Payment processing is not available at this time. Please try again later or contact support if the issue persists.
-              </div>
-            )}
-          </ErrorBoundary>
-        )}
+            ) : null}
+          </ErrorBoundary>
+          ) : (
+            <div className="p-4 text-center text-muted-foreground">Loading payment form...</div>
+          )
+        ) : (
+          <div className="p-4 text-center text-muted-foreground">
+            Payment processing is not available at this time. Please try again later or contact support if the issue persists.
+          </div>
+        )}

47-48: Remove unused destructured prop cardToDelete.

Destructuring to _cardToDelete is unnecessary and may trigger TS unused warnings.

-  cardToDelete: _cardToDelete,

No other code references it here.


93-100: DRY the Elements appearance config (optional).

Appearance theme/variables are duplicated across files. Consider extracting a helper getStripeAppearance(isDarkMode) to keep it consistent.

apps/deploy-web/src/hooks/use3DSecure.ts (2)

48-90: Flow is resilient; consider returning structured errors for better UX.

You map by substrings later. If ThreeDSecureModal passes Stripe error codes/types, you can produce more precise messages.

Cross-file follow-up (non-breaking): change onError to accept { code?: string; message: string } and branch on known Stripe codes.


96-104: Error mapping: broaden patterns carefully.

String contains checks can misclassify; prefer exact code checks when available. Keep as-is if upstream can’t pass codes yet.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (1)

2-2: Avoid deep import paths for types

Deep paths can be brittle. If the SDK re-exports types, prefer importing from the package root to reduce churn.

-import type { PaymentMethod } from "@akashnetwork/http-sdk/src/stripe/stripe.types";
+import type { PaymentMethod } from "@akashnetwork/http-sdk";

If not re-exported today, ignore this.

apps/deploy-web/src/pages/payment.tsx (2)

88-96: Don’t proceed with payment when there’s a validation error

Guard against amountError to avoid sending invalid requests.

   const handlePayment = async (paymentMethodId: string) => {
     if (!amount) return;
+    if (amountError) return;
     if (!selectedPaymentMethodId || !paymentMethods.some(method => method.id === selectedPaymentMethodId)) return;
 
     // Capture the submitted amount before starting the payment flow
     submittedAmountRef.current = amount;

170-186: Refresh list after successful removal

Ensure UI reflects the deletion immediately.

   const confirmRemovePaymentMethod = async () => {
@@
     try {
       await removePaymentMethod.mutateAsync(cardToDelete);
       setSelectedPaymentMethodId(undefined);
       enqueueSnackbar(<Snackbar title="Payment method removed successfully" iconVariant="success" />, { variant: "success" });
+      await refetchPaymentMethods();
     } catch (error: unknown) {
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx (1)

367-411: Typed mocks: consider jest-mock-extended for safer mocks

Your manual jest.fn mocks work, but jest-mock-extended improves type safety and reduces boilerplate. Optional adoption.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (2)

46-51: Expose full threeDSecure surface in the public type (matches what you pass and what tests assert)

Include start/close and optional isLoading to reflect the actual object.

     threeDSecure: {
       isOpen: boolean;
-      threeDSData: { clientSecret: string; paymentIntentId: string; paymentMethodId: string } | null;
-      handle3DSSuccess: () => Promise<void>;
-      handle3DSError: (error: string) => void;
+      threeDSData: { clientSecret: string; paymentIntentId: string; paymentMethodId: string } | null;
+      start3DSecure: (data: { clientSecret: string; paymentIntentId?: string; paymentMethodId?: string }) => void;
+      close3DSecure: () => void;
+      handle3DSSuccess: () => Promise<void>;
+      handle3DSError: (error: string) => void;
+      isLoading?: boolean;
     };

113-138: 3DS start: great validation; minor improvement — don’t pass empty IDs

You already block when both IDs are missing. To reduce noisy post-3DS validation calls, avoid passing empty strings; pass only present IDs and update the hook to skip server validation if either ID is missing.

Suggested change in use3DSecure.ts (outside this file):

// inside handle3DSSuccess
if (threeDSData?.paymentMethodId && threeDSData?.paymentIntentId) {
  await validatePaymentMethodAfter3DS.mutateAsync({
    paymentMethodId: threeDSData.paymentMethodId,
    paymentIntentId: threeDSData.paymentIntentId
  });
}

And here:

-      threeDSecure.start3DSecure({
-        clientSecret: clientSecret.trim(),
-        paymentIntentId: paymentIntentId?.trim() || "",
-        paymentMethodId: paymentMethodId?.trim() || ""
-      });
+      const data: { clientSecret: string; paymentIntentId?: string; paymentMethodId?: string } = {
+        clientSecret: clientSecret.trim()
+      };
+      if (paymentIntentId?.trim()) data.paymentIntentId = paymentIntentId.trim();
+      if (paymentMethodId?.trim()) data.paymentMethodId = paymentMethodId.trim();
+      threeDSecure.start3DSecure(data);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc9e9ac and 90447eb.

📒 Files selected for processing (31)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (1 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts (5 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx (6 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (5 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (4 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsx (3 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (2 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx (1 hunks)
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodCard/index.ts (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx (1 hunks)
  • apps/deploy-web/src/components/shared/PaymentMethodsList/index.ts (1 hunks)
  • apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx (1 hunks)
  • apps/deploy-web/src/components/user/payment/PaymentForm.tsx (1 hunks)
  • apps/deploy-web/src/components/user/payment/PaymentMethodsList.tsx (0 hunks)
  • apps/deploy-web/src/components/user/payment/index.ts (0 hunks)
  • apps/deploy-web/src/hooks/use3DSecure.ts (1 hunks)
  • apps/deploy-web/src/hooks/useManagedWallet.ts (2 hunks)
  • apps/deploy-web/src/pages/payment.tsx (7 hunks)
  • apps/deploy-web/src/queries/useManagedWalletQuery.ts (1 hunks)
  • apps/deploy-web/src/queries/usePaymentQueries.ts (4 hunks)
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.types.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/deploy-web/src/components/user/payment/index.ts
  • apps/deploy-web/src/components/user/payment/PaymentMethodsList.tsx
🚧 Files skipped from review as they are similar to previous changes (16)
  • apps/deploy-web/src/components/shared/PaymentMethodCard/index.ts
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodsList/index.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx
  • apps/deploy-web/src/hooks/useManagedWallet.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx
  • apps/deploy-web/src/queries/usePaymentQueries.ts
  • packages/http-sdk/src/stripe/stripe.types.ts
  • apps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx
  • apps/deploy-web/src/components/user/payment/PaymentForm.tsx
  • packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts
  • apps/deploy-web/src/queries/useManagedWalletQuery.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx
  • apps/deploy-web/src/pages/payment.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
  • apps/deploy-web/src/hooks/use3DSecure.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx
  • apps/deploy-web/src/pages/payment.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
  • apps/deploy-web/src/hooks/use3DSecure.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
apps/{deploy-web,provider-console}/**/*.spec.tsx

📄 CodeRabbit inference engine (.cursor/rules/query-by-in-tests.mdc)

Use queryBy methods instead of getBy methods in test expectations in .spec.tsx files

Files:

  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-18T07:50:31.197Z
Learnt from: baktun14
PR: akash-network/console#1933
File: apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx:43-63
Timestamp: 2025-09-18T07:50:31.197Z
Learning: PaymentVerificationCard component is wrapped by Stripe Elements provider in its parent component PaymentMethodStep, so PaymentMethodForm inside PaymentVerificationCard has access to Stripe context without needing its own Elements wrapper.

Applied to files:

  • apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
  • apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
  • apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
📚 Learning: 2025-09-18T07:44:29.058Z
Learnt from: baktun14
PR: akash-network/console#1933
File: apps/deploy-web/src/pages/payment.tsx:92-116
Timestamp: 2025-09-18T07:44:29.058Z
Learning: In the payment system, discount calculation is not applied during payment processing. Instead, when users claim coupons/discounts, the discount amount is applied directly to their account balance. Therefore, the payment flow should charge parseFloat(amount) as entered, not a discounted amount.

Applied to files:

  • apps/deploy-web/src/pages/payment.tsx
🧬 Code graph analysis (7)
apps/deploy-web/src/pages/payment.tsx (4)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
  • use3DSecure (30-125)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (2)
  • confirmPayment (53-97)
  • removePaymentMethod (123-145)
packages/http-sdk/src/stripe/stripe.service.ts (2)
  • confirmPayment (46-48)
  • removePaymentMethod (32-34)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)
  • ThreeDSecurePopup (21-50)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (5)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
  • ThreeDSecureData (6-10)
apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx (1)
  • useServices (27-29)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx (1)
  • ThreeDSecurePopup (21-50)
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (1)
  • PaymentVerificationCard (17-65)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (1)
  • PaymentMethodsDisplay (21-49)
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx (2)
packages/ui/components/spinner.tsx (1)
  • Spinner (10-39)
apps/deploy-web/src/context/ServicesProvider/ServicesProvider.tsx (1)
  • useServices (27-29)
apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (4)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • SetupIntentResponse (66-68)
apps/deploy-web/src/hooks/useUser.ts (1)
  • useUser (7-20)
apps/deploy-web/src/queries/usePaymentQueries.ts (1)
  • usePaymentMethodsQuery (16-26)
apps/deploy-web/src/components/shared/PaymentMethodForm/PaymentMethodForm.tsx (1)
  • PaymentMethodForm (13-83)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx (3)
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts (2)
  • createWallet (26-30)
  • ApiWalletWithOptional3DS (18-23)
apps/deploy-web/src/utils/errorUtils.ts (1)
  • extractErrorMessage (6-28)
apps/deploy-web/src/types/errors.ts (1)
  • AppError (26-26)
apps/deploy-web/src/hooks/use3DSecure.ts (1)
apps/deploy-web/src/queries/usePaymentQueries.ts (1)
  • usePaymentMutations (68-130)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (5)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsx (1)
  • EmptyPaymentMethods (5-15)
apps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsx (1)
  • PaymentMethodsList (20-68)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsx (1)
  • ErrorAlert (12-26)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsx (1)
  • TrialStartButton (10-16)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsx (1)
  • TermsAndConditions (6-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (15)
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsx (2)

9-11: LGTM: simple, clear conditional render

Returning null when hidden keeps the tree clean. Component is cohesive.


13-13: Confirm Alert announces itself (role/aria-live)

If the design-system Alert doesn’t set appropriate ARIA (role="alert", aria-live), add them here.

-    <Alert className="mx-auto max-w-md text-left" variant="warning">
+    <Alert className="mx-auto max-w-md text-left" variant="warning" role="alert" aria-live="assertive">
apps/api/src/billing/services/stripe/stripe.service.ts (1)

750-752: Good: enforce PaymentIntent ownership (blocks cross‑tenant misuse).

The explicit customer check is correct and uses the right 403 semantics.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)

196-201: Good: verify payment method ownership before 3DS validation.

Prevents clients from validating someone else’s payment method.

apps/api/src/billing/services/stripe/stripe.service.spec.ts (4)

1269-1272: Tests correctly assert success path with customer on PaymentIntent.


1289-1293: Tests cover requires_capture with customer bound.


1309-1313: Declined path test updated with customer — good coverage.


1339-1342: User-not-found path includes customer — OK.

apps/deploy-web/src/components/user/payment/AddPaymentMethodPopup.tsx (1)

27-27: Good: force-remount Elements on clientSecret changes.

Adding key={clientSecret} prevents stale PaymentElement state across SetupIntent rotations. Looks correct given the guard at Line 23.

apps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx (2)

30-41: Provide a visible loading state while waiting for setupIntent.

The fallback shows “Loading payment form...”, but if this component is not rendered because the parent gates on setupIntent (see PaymentMethodStep), users may see an empty area. Coordinate with the parent to always show a loader when setupIntent is undefined.


43-63: Stripe Elements wrapper verified — no action required.
PaymentVerificationCard is rendered inside in PaymentMethodStep (apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx, ~lines 88–103); no other usages found.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx (2)

89-101: Good: Elements keyed by clientSecret.

Prevents stale PaymentElement across SetupIntent rotations.


84-112: Audit all Elements wrappers for missing key={clientSecret}.

To prevent stale state bugs, ensure every Elements wrapping PaymentElement or 3DS uses a key tied to the active clientSecret.

apps/deploy-web/src/hooks/use3DSecure.ts (1)

30-37: LGTM.

API surface and defaults are sensible; no any/casts; callbacks are memoized.

apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsx (1)

44-45: Confirm gating: should Start Trial require a validated card instead of any card?

Button is disabled when no payment methods or while loading. If trials require a validated card (post-3DS), consider gating on hasValidatedCard to prevent premature starts. If intentional, please confirm.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)

196-203: Also verify PaymentIntent ownership (defense-in-depth).

Service enforces this, but checking here yields clearer 403s and consistent authZ at the boundary.

   // Verify payment method ownership
   const paymentMethod = await this.stripe.paymentMethods.retrieve(paymentMethodId);
   const customerId = typeof paymentMethod.customer === "string" ? paymentMethod.customer : paymentMethod.customer?.id;
   assert(customerId === currentUser.stripeCustomerId, 403, "Payment method does not belong to the user");

+  // Verify payment intent ownership
+  const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentIntentId);
+  const intentCustomerId =
+    typeof paymentIntent.customer === "string" ? paymentIntent.customer : paymentIntent.customer?.id;
+  assert(intentCustomerId === currentUser.stripeCustomerId, 403, "Payment intent does not belong to the user");
apps/api/src/billing/services/stripe/stripe.service.ts (1)

746-756: Block mismatched-card validation: assert PI.payment_method === paymentMethodId.

A user could complete 3DS on one card but pass a different paymentMethodId; we’d incorrectly mark that other method as validated. Guard by checking the PaymentIntent references the same payment method before calling markPaymentMethodAsValidated.

   const paymentIntent = await this.paymentIntents.retrieve(paymentIntentId);

   const paymentIntentCustomerId = typeof paymentIntent.customer === "string" ? paymentIntent.customer : paymentIntent.customer?.id;
   assert(paymentIntentCustomerId === customerId, 403, "Payment intent does not belong to the user");

+  const piPaymentMethodId =
+    typeof paymentIntent.payment_method === "string"
+      ? paymentIntent.payment_method
+      : paymentIntent.payment_method?.id;
+  assert(piPaymentMethodId === paymentMethodId, 400, "Payment intent does not reference the provided payment method");
🧹 Nitpick comments (1)
apps/api/src/billing/services/stripe/stripe.service.ts (1)

746-752: Optional hardening: assert metadata.type === "payment_method_validation".

Prevents reusing unrelated PIs (e.g., a prior successful transaction) to bypass validation.

   const paymentIntent = await this.paymentIntents.retrieve(paymentIntentId);
+  assert(paymentIntent.metadata?.type === "payment_method_validation", 400, "Payment intent is not a validation intent");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90447eb and ee5f6a3.

📒 Files selected for processing (3)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (1 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts (6 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: provider-proxy-ci
  • GitHub Check: indexer-ci
  • GitHub Check: validate (apps/notifications) / validate-unsafe
  • GitHub Check: validate (apps/deploy-web) / validate-unsafe
  • GitHub Check: validate (packages) / validate-unsafe
  • GitHub Check: validate (apps/log-collector) / validate-unsafe
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (2)
apps/api/src/billing/services/stripe/stripe.service.ts (1)

765-776: Good: method now returns the actual validation outcome.

Returning { success: boolean } fixes the earlier always-true controller response and aligns API with clients.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)

196-203: Good: ownership check for payment method and returning service result.

Verifies PM ownership and forwards the service’s { success }. Solid.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/api/src/billing/services/stripe/stripe.service.ts (4)

753-756: Harden against reusing unrelated PaymentIntents (check metadata + capture_method).

To ensure only validation PIs can mark a method “validated,” also assert a marker and expected capture method. This prevents using arbitrary purchase PIs for validation.

Apply this diff after the existing equality checks:

       // Ensure the PaymentIntent references the same payment method being validated
       const paymentIntentPaymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
       assert(paymentIntentPaymentMethodId === paymentMethodId, 403, "Payment intent does not reference the provided payment method");
+
+      // Optional but recommended: only allow our validation intents
+      assert(paymentIntent.capture_method === "manual", 400, "Payment intent not a validation authorization");
+      assert(paymentIntent.metadata?.type === "payment_method_validation", 400, "Payment intent not issued for payment method validation");
+      // If you want stricter scoping, also pin amount/currency:
+      // assert(paymentIntent.amount === 100 && paymentIntent.currency === "usd", 400, "Unexpected validation intent amount/currency");

751-752: Status code semantics: use 400 for wrong PI↔PM pairing; keep 403 for cross-customer mismatch.

403 is perfect for cross-customer access. The PI↔PM mismatch is a bad request from the same user; 400 better communicates that.

-assert(paymentIntentPaymentMethodId === paymentMethodId, 403, "Payment intent does not reference the provided payment method");
+assert(paymentIntentPaymentMethodId === paymentMethodId, 400, "Payment intent does not reference the provided payment method");

Also applies to: 755-756


757-760: Optionally void the auth to release the $1 hold after validation.

If this PI is our validation auth (manual capture), canceling it frees the hold immediately instead of waiting for expiry. Gate on metadata/type to avoid touching real purchases.

       if (paymentIntent.status === "succeeded" || paymentIntent.status === "requires_capture") {
         // Payment intent was successfully authenticated, mark payment method as validated
         await this.markPaymentMethodAsValidated(customerId, paymentMethodId, paymentIntentId);
+
+        // Optional: release manual-capture hold for validation intents
+        if (
+          paymentIntent.capture_method === "manual" &&
+          paymentIntent.metadata?.type === "payment_method_validation" &&
+          paymentIntent.status === "requires_capture"
+        ) {
+          try {
+            await this.paymentIntents.cancel(paymentIntentId);
+          } catch (cancelError) {
+            logger.warn({
+              event: "VALIDATION_PI_CANCEL_FAILED",
+              customerId,
+              paymentMethodId,
+              paymentIntentId,
+              error: cancelError instanceof Error ? cancelError.message : String(cancelError)
+            });
+          }
+        }

782-790: Enrich Stripe error logging with structured fields.

Capture error.type/code/decline_code without leaking sensitive payloads. Helps SRE triage.

     } catch (error) {
-      logger.error({
-        event: "FAILED_TO_CHECK_PAYMENT_INTENT_AFTER_3DS",
-        customerId,
-        paymentMethodId,
-        paymentIntentId,
-        error: error instanceof Error ? error.message : String(error)
-      });
+      const base = {
+        event: "FAILED_TO_CHECK_PAYMENT_INTENT_AFTER_3DS",
+        customerId,
+        paymentMethodId,
+        paymentIntentId
+      };
+      if (error instanceof Stripe.errors.StripeError) {
+        logger.error({
+          ...base,
+          stripeError: {
+            type: error.type,
+            code: error.code,
+            decline_code: error.decline_code,
+            param: error.param,
+            message: error.message
+          }
+        });
+      } else {
+        logger.error({ ...base, error: error instanceof Error ? error.message : String(error) });
+      }
       throw error;
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee5f6a3 and 4da2e27.

📒 Files selected for processing (2)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts (13 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/billing/services/stripe/stripe.service.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/billing/services/stripe/stripe.service.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (2)
apps/api/src/billing/services/stripe/stripe.service.ts (2)

750-756: Good guardrails: customer and payment method ownership checks.

Verifying PI.customer and PI.payment_method match the provided IDs closes the prior gap. Nice.


769-780: Resolved — return shape ({ success: boolean }) confirmed across controller, http‑sdk, and UI.
Verified: apps/api/src/billing/services/stripe/stripe.service.ts returns { success: boolean }, controller passes it through (apps/api/src/billing/controllers/stripe/stripe.controller.ts), packages/http-sdk (src/stripe/stripe.service.ts and managed-wallet-http.service.ts) expose the same signature, and apps/deploy-web/src/queries/usePaymentQueries.ts consumes it; tests assert { success: true|false }.

@baktun14 baktun14 merged commit 4c7ce23 into main Sep 23, 2025
61 of 63 checks passed
@baktun14 baktun14 deleted the features/billing-3d-secure branch September 23, 2025 06:41
stalniy pushed a commit that referenced this pull request Nov 20, 2025
* feat(billing): implement 3D Secure payment handling

- Add ThreeDSecureModal and ThreeDSecurePopup components
- Implement use3DSecure hook for payment authentication flow
- Update payment page with 3D Secure integration
- Add support for 3D Secure authentication in payment flow

* feat(billing): refactor payment UI components and improve UX

- Refactor PaymentMethodsDisplay components with better organization
- Add new UI components: EmptyPaymentMethods, ErrorAlert, PaymentMethodCard
- Add TermsAndConditions, TrialStartButton, ValidationWarning components
- Update PaymentMethodContainer and PaymentMethodStep with improved UX
- Enhance PaymentVerificationCard with better validation flow
- Update payment queries and managed wallet integration
- Improve payment form user experience

* fix(billing): fix type

* fix(billing): refactor payment list components

* fix(billing): pr fixes

* fix(billing): 3d secure fixes

* fix(billing): fix tests

* fix(billing): update validatePaymentMethodAfter3DS to return success status

* fix(billing): enhance payment intent validation to check payment method reference
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

Comments