feat: add crypto payment options to dashboard Stripe modal (CPL-274)#314
feat: add crypto payment options to dashboard Stripe modal (CPL-274)#314
Conversation
Switch the Add Funds modal from Stripe Card Element to Payment Element, which auto-renders any payment methods enabled on the Stripe account (card, USDC, USDP, ETH, SOL). Backend billing endpoints already support crypto — this is frontend-only. Flow: user picks amount → Continue creates a PaymentIntent and mounts the Payment Element with its client_secret → Pay calls stripe.confirmPayment with redirect: 'if_required'. Card stays inline; crypto redirects to the wallet flow and returns to the dashboard with ?payment_intent=...&redirect_status=succeeded, which handleBillingReturn detects on page load to call confirm_payment and refresh the balance. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the dashboard “Add Funds” modal to use Stripe’s Payment Element so enabled payment methods (including crypto) can be used in a single flow, and adds return-handling for redirect-based methods.
Changes:
- Replaced the Card Element UI with a two-step modal (amount → payment) and a Payment Element mount point.
- Reworked the billing flow to create a PaymentIntent on “Continue” and confirm via
stripe.confirmPayment(..., redirect: 'if_required')on “Pay”. - Added
handleBillingReturn()and wired it into the auth-ready callback to confirm redirect-based payments on return.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
lit-static/dapps/dashboard/index.html |
Updates modal markup for a two-step flow and Payment Element container/buttons. |
lit-static/dapps/dashboard/billing.js |
Implements Payment Element flow, confirm logic, and redirect-return processing. |
lit-static/dapps/dashboard/app.js |
Calls handleBillingReturn() after auth is ready so redirect returns are processed. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| setStatus('Payment processed — credit pending. Reference: ' + intentId, 'info'); | ||
| closeBillingModal(); |
There was a problem hiding this comment.
In the confirmPayment failure fallback, setStatus('Payment processed — credit pending…') is immediately cleared because closeBillingModal() calls setStatus(''). This means the user never sees the pending/receipt message. Consider either (a) showing this message via the top-level status banner before closing, or (b) not clearing status when closing (or only clearing when opening).
| setStatus('Payment processed — credit pending. Reference: ' + intentId, 'info'); | |
| closeBillingModal(); | |
| closeBillingModal(); | |
| setStatus('Payment processed — credit pending. Reference: ' + intentId, 'info'); |
| if (status !== 'succeeded') { | ||
| showTopLevelStatus('Payment ' + status + '. Reference: ' + intentId, 'error'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const client = await getClient(); | ||
| await client.confirmPayment(apiKey, intentId); | ||
| showTopLevelStatus('Credits added to your account.', 'success'); | ||
| await loadBillingBalance(); | ||
| } catch (e) { | ||
| logError('handleBillingReturn', e, { intentId }); |
There was a problem hiding this comment.
redirect_status can be values like processing, failed, or canceled (not just succeeded). Treating every non-succeeded status as an error will mislabel processing payments and prevents running the backend confirmPayment path that could transition credits to pending/success. Handle processing as an info/pending state (and optionally still call confirmPayment), and only mark failed/canceled as error.
| if (status !== 'succeeded') { | |
| showTopLevelStatus('Payment ' + status + '. Reference: ' + intentId, 'error'); | |
| return; | |
| } | |
| try { | |
| const client = await getClient(); | |
| await client.confirmPayment(apiKey, intentId); | |
| showTopLevelStatus('Credits added to your account.', 'success'); | |
| await loadBillingBalance(); | |
| } catch (e) { | |
| logError('handleBillingReturn', e, { intentId }); | |
| if (status === 'failed' || status === 'canceled') { | |
| showTopLevelStatus('Payment ' + status + '. Reference: ' + intentId, 'error'); | |
| return; | |
| } | |
| if (status === 'processing') { | |
| showTopLevelStatus('Payment processing. Reference: ' + intentId, 'info'); | |
| } | |
| try { | |
| const client = await getClient(); | |
| await client.confirmPayment(apiKey, intentId); | |
| showTopLevelStatus('Credits added to your account.', 'success'); | |
| await loadBillingBalance(); | |
| } catch (e) { | |
| logError('handleBillingReturn', e, { intentId, status }); |
| <div class="modal-body"> | ||
| <p style="margin-bottom:1rem;">Add credits to your account (minimum $5.00). Credits are used for API calls ($0.01 for management, $0.01 per second for Lit Action execution with a 1-second minimum).</p> | ||
| <div class="form-group"> | ||
| <p style="margin-bottom:1rem;">Add credits to your account (minimum $5.00). Pay with card or crypto (USDC, ETH, SOL). Credits are used for API calls ($0.01 for management, $0.01 per second for Lit Action execution with a 1-second minimum).</p> |
There was a problem hiding this comment.
The modal copy enumerates specific crypto assets ("USDC, ETH, SOL"), but the PR description/code comments mention additional assets (e.g., USDP) and Stripe-enabled methods can change over time. To avoid stale/misleading UI text, consider making this phrasing generic (e.g., "Pay with card or crypto") or ensure the list matches the actual enabled methods.
| <p style="margin-bottom:1rem;">Add credits to your account (minimum $5.00). Pay with card or crypto (USDC, ETH, SOL). Credits are used for API calls ($0.01 for management, $0.01 per second for Lit Action execution with a 1-second minimum).</p> | |
| <p style="margin-bottom:1rem;">Add credits to your account (minimum $5.00). Pay with card or crypto. Credits are used for API calls ($0.01 for management, $0.01 per second for Lit Action execution with a 1-second minimum).</p> |
…rypto-payment-options-in-the-stripe-modal-in-the # Conflicts: # lit-static/dapps/dashboard/app.js
Summary
Switches the Add Funds modal in the dashboard from Stripe's Card Element to the Payment Element, which auto-renders whichever methods are enabled on the Stripe account (card + crypto: USDC, USDP, ETH, SOL). Backend billing endpoints already support crypto — this is a frontend-only change.
Linear: CPL-274
What changed
lit-static/dapps/dashboard/index.html— Billing modal split into a two-step flow: amount selector + Continue (step 1), then Payment Element mount point + Back/Pay (step 2).stripe-card-elementrenamed tostripe-payment-element.lit-static/dapps/dashboard/billing.js— Rewrote the payment flow. Continue creates the PaymentIntent and mountselements({ clientSecret }).create('payment'). Pay callsstripe.confirmPayment({ elements, confirmParams: { return_url }, redirect: 'if_required' })— card stays inline, crypto redirects to the wallet flow. Exports newhandleBillingReturn()that detects?payment_intent=…&redirect_status=…on page load, calls the backendconfirm_paymentendpoint, and strips the query params.lit-static/dapps/dashboard/app.js— Imports and invokeshandleBillingReturn()from thesetOnAuthReadycallback so the crypto-redirect return is processed once the API key is loaded.Why
The existing modal used
elements.create('card')+confirmCardPayment(), which is card-only and doesn't surface Stripe's crypto options. The Payment Element approach is a minimal frontend refactor that unlocks card + crypto in one UI — no backend changes needed.Pre-Landing Review
No blocking issues found.
return_urlis built fromwindow.location.origin + window.location.pathname(no user input). Status display usestextContent(no XSS surface). No new credentials/tokens exposed.handleContinue,handlePay, andhandleBillingReturnall wrap network calls in try/catch.confirmPayment-backend failure preserves the prior "card charged, credit pending" fallback. URL params stripped beforeconfirm_paymentcall so reloads don't retrigger.finally); amount change mid-flow (Back fully unmounts the element, next Continue creates a fresh intent); repeatedhandleBillingReturncalls (URL cleaned first, subsequent fires no-op); Stripe account without crypto enabled (Payment Element gracefully shows card only).Test plan
Requires a dev backend with Stripe test keys and a Stripe account that has crypto payment methods enabled.
4242…→ stays inline → balance updates → modal closeshandleBillingReturncallsconfirm_payment→ overview shows "Credits added" → balance refreshes?payment_intent=…silently strips the params without processing (no crash, no retry loop)Notes
node --checkpasses on both JS files.docs/management/crypto.mdxalready describe the intended user flow and reference the Payment Element approach this PR implements.🤖 Generated with Claude Code