feat(billing): add 3dsecure payments ui#1933
Conversation
WalkthroughImplements 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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
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: Avoidany[]in TS tests; use a proper typeGuideline: never use
any. Type the test data asPartial<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
PaymentMethodisn’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('.')isNaN; 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 pathMinor polish: specify the cached type and null-out any prior wallet to avoid stale UI when
requires3DSis 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, addtarget="_blank"andrel="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 ATAdd
aria-hiddenso 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-hiddenAvoid 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 defaultAllows 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-activationEnsure non-submit semantics and disable while loading; add
aria-busyfor 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 loggerExcessive
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 interfacesReplace hand-rolled
StripeError,PaymentIntent, andAuthenticationResultwith@stripe/stripe-jstypes 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 thetitleprop (currently ignored)The header hardcodes “Secure Authentication”. Use the provided
titlefor 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
titlethrough from the parent as you already do.Also applies to: 186-195
15-27: Remove or wire unused props:paymentIntentId,onClose
paymentIntentIdis never read, andonCloseis 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-rendersIf 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 formattingHandle 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 callbackonSuccess 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 invalidPrevent 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 discountsPresence 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 isOpenThis 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 confirmPaymentPage 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 typesPer 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 dependenciesCurrent 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 typingAvoids 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 valueMake 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.start3DSecureinside the callback passed touse3DSecureworks but is fragile. Capturestart3DSecureafter 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
📒 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.tsxapps/deploy-web/src/queries/useManagedWalletQuery.tspackages/http-sdk/src/stripe/stripe.types.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/deploy-web/src/hooks/use3DSecure.tsapps/deploy-web/src/components/user/payment/PaymentForm.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsxapps/deploy-web/src/hooks/useManagedWallet.tsapps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsxapps/deploy-web/src/pages/payment.tsxpackages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsxapps/deploy-web/src/queries/usePaymentQueries.tsapps/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.tsxapps/deploy-web/src/queries/useManagedWalletQuery.tspackages/http-sdk/src/stripe/stripe.types.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/EmptyPaymentMethods.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ErrorAlert.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TrialStartButton.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/deploy-web/src/hooks/use3DSecure.tsapps/deploy-web/src/components/user/payment/PaymentForm.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsList.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/TermsAndConditions.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.test.tsxapps/deploy-web/src/hooks/useManagedWallet.tsapps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsxapps/deploy-web/src/pages/payment.tsxpackages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsxapps/deploy-web/src/queries/usePaymentQueries.tsapps/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, usejest-mock-extendedto create mocks and pass mocks as dependencies to the service under test.
**/*.spec.{ts,tsx}: Usesetupfunction instead ofbeforeEachin test files
setupfunction must be at the bottom of the rootdescribeblock in test files
setupfunction creates an object under test and returns it
setupfunction should accept a single parameter with inline type definition
Don't use shared state insetupfunction
Don't specify return type ofsetupfunction
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
queryBymethods instead ofgetBymethods in test expectations in.spec.tsxfiles
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 correctPosting
{ paymentMethodId, paymentIntentId }withwithCredentials: trueand unwrapping viaextractApiDataaligns with the http-sdk conventions.apps/deploy-web/src/hooks/use3DSecure.ts (1)
30-37: Hook API and state management look solidClear 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 forhasPaymentMethodThe 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 compositionTyped props, stable keys, and clean mapping.
apps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodCard.tsx (1)
6-10: Confirm type shape for “validated” fieldEnsure 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
onSuccessis 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.
apps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsx
Show resolved
Hide resolved
...ploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
Show resolved
Hide resolved
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsx
Show resolved
Hide resolved
packages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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 propsexport 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 missingCurrently 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#somealready 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 branchThe 3DS handling block is duplicated in
onSuccessandhandleNext. 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
📒 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.tsapps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsxapps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsxapps/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.tsapps/deploy-web/src/components/shared/PaymentMethodCard/PaymentMethodCard.tsxapps/deploy-web/src/components/shared/PaymentMethodsList/PaymentMethodsList.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/PaymentMethodsDisplay.tsxapps/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 solidNice integration of
use3DSecure, clear async flow, and consistent user-facing errors viaextractErrorMessage+ 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 correctGood: confirmation gating, optimistic closure, refetch, and user-visible error handling.
42-51: threeDSecure shape verification — matches hook returnuse3DSecure (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.
...deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
Show resolved
Hide resolved
...deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
Show resolved
Hide resolved
...deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
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-jstypes.+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 providedtitleprop in the UI.
titleis 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 unusedpaymentIntentIdplumb-through from ThreeDSecureFormThreeDSecureModal accepts and forwards paymentIntentId, but ThreeDSecureForm renames it to
_paymentIntentIdand 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
📒 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.tsxapps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsxpackages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.tsapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/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.tsxapps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecurePopup.tsxpackages/http-sdk/src/managed-wallet-http/managed-wallet-http.service.tsapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/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:colorSuccessset 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 */
...ploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsx
Show resolved
Hide resolved
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
Outdated
Show resolved
Hide resolved
apps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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
📒 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.tsapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsxapps/deploy-web/src/pages/payment.tsxapps/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.tsapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsxapps/deploy-web/src/pages/payment.tsxapps/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.
...deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx
Show resolved
Hide resolved
- 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
cc9e9ac to
90447eb
Compare
There was a problem hiding this comment.
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” checksPer 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 allanyusage; tests are also subject to our TS rulesReplace 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:onNextis async but typed as() => voidUpdate 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:onNextis awaited here — make the prop type Promise-awareTests 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 readersThis 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 overridableHardcoded 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 typesDeep 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 errorGuard 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 removalEnsure 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: considerjest-mock-extendedfor safer mocksYour manual jest.fn mocks work, but
jest-mock-extendedimproves 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 IDsYou 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
📒 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.tsxapps/api/src/billing/controllers/stripe/stripe.controller.tsapps/api/src/billing/services/stripe/stripe.service.tsapps/api/src/billing/services/stripe/stripe.service.spec.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsxapps/deploy-web/src/pages/payment.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsxapps/deploy-web/src/hooks/use3DSecure.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsxapps/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.tsxapps/api/src/billing/controllers/stripe/stripe.controller.tsapps/api/src/billing/services/stripe/stripe.service.tsapps/api/src/billing/services/stripe/stripe.service.spec.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodsDisplay/ValidationWarning.tsxapps/deploy-web/src/pages/payment.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/deploy-web/src/components/onboarding/steps/PaymentVerificationCard/PaymentVerificationCard.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsxapps/deploy-web/src/hooks/use3DSecure.tsapps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.spec.tsxapps/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, usejest-mock-extendedto create mocks and pass mocks as dependencies to the service under test.
**/*.spec.{ts,tsx}: Usesetupfunction instead ofbeforeEachin test files
setupfunction must be at the bottom of the rootdescribeblock in test files
setupfunction creates an object under test and returns it
setupfunction should accept a single parameter with inline type definition
Don't use shared state insetupfunction
Don't specify return type ofsetupfunction
Files:
apps/api/src/billing/services/stripe/stripe.service.spec.tsapps/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
queryBymethods instead ofgetBymethods in test expectations in.spec.tsxfiles
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.tsxapps/deploy-web/src/components/onboarding/steps/PaymentMethodStep/PaymentMethodStep.tsxapps/deploy-web/src/components/shared/PaymentMethodForm/ThreeDSecureModal.tsxapps/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 renderReturning 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.
There was a problem hiding this comment.
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
📒 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.tsapps/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.tsapps/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.
There was a problem hiding this comment.
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
📒 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 }.
* 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
#1886
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests