-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add crypto payment options to dashboard Stripe modal (CPL-274) #314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,20 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Billing — Stripe integration, payment flow. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Uses Stripe Payment Element which auto-renders whichever methods | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * are enabled on the account (card, USDC, USDP, ETH, SOL). Crypto | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * payments use a redirect flow; card payments complete inline when | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * no additional action is required. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getApiKey, getClient, hasUsageKeyOverride } from './auth.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { formatError, logError } from './ui-utils.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _stripe = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _stripeCard = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _publishableKey = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _elements = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _paymentElement = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _paymentIntentId = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _billingAvailable = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let _billingCheckedAt = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,7 +28,8 @@ export async function checkBillingAvailable() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = await getClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await client.getStripeConfig(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cfg = await client.getStripeConfig(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _publishableKey = cfg.publishable_key; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _billingAvailable = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (_) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _billingAvailable = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -94,125 +103,229 @@ async function loadBillingBalance() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function setModalStep(step) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const amountGroup = document.getElementById('billing-amount-group'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const paymentGroup = document.getElementById('billing-payment-group'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const continueBtn = document.getElementById('billing-continue-btn'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const payBtn = document.getElementById('billing-pay-btn'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const backBtn = document.getElementById('billing-back-btn'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (step === 'amount') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (amountGroup) amountGroup.style.display = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (paymentGroup) paymentGroup.style.display = 'none'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (continueBtn) continueBtn.style.display = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (payBtn) payBtn.style.display = 'none'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (backBtn) backBtn.style.display = 'none'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (amountGroup) amountGroup.style.display = 'none'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (paymentGroup) paymentGroup.style.display = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (continueBtn) continueBtn.style.display = 'none'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (payBtn) payBtn.style.display = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (backBtn) backBtn.style.display = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function setStatus(message, kind) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const el = document.getElementById('billing-modal-status'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!el) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!message) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.style.display = 'none'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.textContent = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.textContent = message; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.className = 'status ' + (kind || 'info'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.style.display = 'block'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function resetPaymentElement() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_paymentElement) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { _paymentElement.unmount(); } catch (_) { /* ignore */ } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { _paymentElement.destroy(); } catch (_) { /* ignore */ } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _paymentElement = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _elements = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _paymentIntentId = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function ensureStripe() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_stripe) return _stripe; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!_publishableKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = await getClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cfg = await client.getStripeConfig(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _publishableKey = cfg.publishable_key; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _stripe = Stripe(_publishableKey); // eslint-disable-line no-undef | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return _stripe; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function openAddFundsModal() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_billingAvailable === false) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const overlay = document.getElementById('billing-modal-overlay'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!overlay) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overlay.classList.add('is-open'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overlay.setAttribute('aria-hidden', 'false'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const statusEl = document.getElementById('billing-modal-status'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (statusEl) { statusEl.style.display = 'none'; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setModalStep('amount'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resetPaymentElement(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ensureStripe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('stripe-init', e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus('Billing not available: ' + formatError(e), 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function closeBillingModal() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const overlay = document.getElementById('billing-modal-overlay'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (overlay) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overlay.classList.remove('is-open'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overlay.setAttribute('aria-hidden', 'true'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resetPaymentElement(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setModalStep('amount'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleContinue() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = getApiKey(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const amountInput = document.getElementById('billing-amount'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const amountCents = parseInt(amountInput?.value || '0', 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!amountCents || amountCents < 500) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus('Minimum amount is $5.00.', 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const continueBtn = document.getElementById('billing-continue-btn'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (continueBtn) continueBtn.disabled = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ensureStripe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = await getClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const intent = await client.createPaymentIntent(apiKey, amountCents); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _paymentIntentId = intent.payment_intent_id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _elements = _stripe.elements({ clientSecret: intent.client_secret }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _paymentElement = _elements.create('payment'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _paymentElement.mount('#stripe-payment-element'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setModalStep('payment'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('createPaymentIntent', e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus('Could not start payment: ' + formatError(e), 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (continueBtn) continueBtn.disabled = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function handleBack() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resetPaymentElement(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setModalStep('amount'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handlePay() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = getApiKey(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey || !_stripe || !_elements || !_paymentIntentId) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const payBtn = document.getElementById('billing-pay-btn'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const backBtn = document.getElementById('billing-back-btn'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (payBtn) payBtn.disabled = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (backBtn) backBtn.disabled = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const intentId = _paymentIntentId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Stripe redirects to return_url for methods that require it (crypto); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // card payments complete inline when redirect: 'if_required' is set. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const returnUrl = window.location.origin + window.location.pathname; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await _stripe.confirmPayment({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elements: _elements, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| confirmParams: { return_url: returnUrl }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirect: 'if_required', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(result.error.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!_stripe) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = await getClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cfg = await client.getStripeConfig(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _stripe = Stripe(cfg.publishable_key); // eslint-disable-line no-undef | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const elements = _stripe.elements(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _stripeCard = elements.create('card'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _stripeCard.mount('#stripe-card-element'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('stripe-init', e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (statusEl) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusEl.textContent = 'Billing not available: ' + formatError(e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusEl.className = 'status error'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusEl.style.display = 'block'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await client.confirmPayment(apiKey, intentId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (confirmErr) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('confirmPayment', confirmErr, { intentId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus('Payment processed — credit pending. Reference: ' + intentId, 'info'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| closeBillingModal(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await loadBillingBalance(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (payBtn) payBtn.disabled = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| closeBillingModal(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await loadBillingBalance(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logError('payment', e, { intentId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setStatus('Payment failed: ' + formatError(e), 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (payBtn) payBtn.disabled = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (backBtn) backBtn.disabled = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function closeBillingModal() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const overlay = document.getElementById('billing-modal-overlay'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (overlay) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overlay.classList.remove('is-open'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overlay.setAttribute('aria-hidden', 'true'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function handleBillingReturn() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const params = new URLSearchParams(window.location.search); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const intentId = params.get('payment_intent'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = params.get('redirect_status'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!intentId || !status) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Strip Stripe redirect params regardless of outcome so reloads don't retrigger. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cleanUrl = window.location.origin + window.location.pathname + window.location.hash; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.history.replaceState({}, '', cleanUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = getApiKey(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+291
to
+302
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the confirmPayment failure fallback,
setStatus('Payment processed — credit pending…')is immediately cleared becausecloseBillingModal()callssetStatus(''). 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).