feat(webapp): let customers manage payment and billing details in-app - #6908
feat(webapp): let customers manage payment and billing details in-app#6908macko911 wants to merge 10 commits into
Conversation
Preview Deploy
|
Orb's customer model supports CC'd additional_emails alongside the primary email, but BillingInvoicingDetails only carried a single email. Add additionalEmails end to end: the shared type, the Orb adapter/schema, the local noop billing client, and server-side validation on PUT /plans/billing/invoicing.
Stripe's PaymentMethod.card already carries brand/exp_month/exp_year; map them onto StripePaymentMethod so the webapp can render a brand + last4 + expiry summary instead of just the last4.
Collapse the four separately-bordered cards (payment method; legal entity + email; address; tax ID) under two muted subheadings into a single panel with internal dividers, matching the approved design — title and 'View all invoices' link on top, payment method, legal entity/email fields, collapsible address and tax ID rows, then save. Also swap the single email input for a chip field (InvoicingEmailsField) so a customer can add and remove multiple billing email addresses.
738a360 to
40aa67c
Compare
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Model additionalEmails as optional (defaulting to []) on the PUT /plans/billing/invoicing body so a caller on the prior single-email payload shape (e.g. a stale webapp bundle mid-deploy) keeps working instead of getting invalid_body. Also cap it at Orb's 49-address additional-email limit and reject a case-insensitive duplicate between the primary email and the additional list.
Match the server's 50-email cap and case-insensitive dedup in the client-side schema so the error surfaces before the request round trip. Also stop treating an uncommitted, invalid address left in the chip input as safe to ignore: track it as a form field (emailsDraft) so a resolver revalidation on submit can't clear the error and let Save silently drop it. Fix the same case-insensitive dedup gap in the chip-add path itself.
A plain span isn't exposed to screen-reader heading navigation. Use an h3 to keep the section landmark without changing styling.
toFormData crashed the whole app with 'not iterable' whenever a customer response omitted additionalEmails — which a real client can hit despite the type's guarantee, e.g. this webapp build outrunning a backend deploy. Default to [] before spreading. Reproduced live: simulated an old-shape response, confirmed the crash, applied the fix, confirmed it renders correctly instead.
Extract toFormData into its own pure-logic module (invoicingFormData.ts, mirroring expandedMetrics.ts/usageChartSeries.ts) so it's testable without pulling in InvoicingDetailsForm's browser-only import chain (window._env via usePlan/useToast), which has no jsdom test setup. Add a regression test for the missing-additionalEmails crash.
- Move the Save button outside the card (Figma has no border around it) by having InvoicingDetailsForm own the Card boundary and render the button as a sibling after it, with the payment-method section passed in as a prop so its independent loading state still works. - Payment method: use variant=secondary size=sm for Edit/Add payment method (was primary/md), vertically center it against the multi-line text block, and fall back to a generic brand label / omit the 'Valid until' line when brand or expMonth/expYear are absent — a real account can hit a backend that hasn't deployed that mapping yet, which was rendering literally as 'Valid until undefined/ed'. - Swap the custom OptionalTag span for the design-system Badge. - Fix 'Payment method' and the address/tax-ID row titles, which were using a heading-sized text-ds-lg/font-ds-medium combo instead of Figma's plain 14px/regular body text (text-body-medium-regular).
The chip container used ComboboxChips' default bg-surface-canvas / border-border-muted, which reads as a much darker fill than every other Input on the form — override to Input's own tokens. Removing a chip (backspace or its own remove button) is a state update, not a native text edit, so the browser's built-in Cmd/Ctrl+Z had nothing to undo. Track removals locally and restore the last one on undo when the draft input is empty.
There was a problem hiding this comment.
2 issues found across 5 files (changes from recent commits).
Confidence score: 3/5
- In
packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx, the undo/restore flow can re-add an email that is already present, creating duplicate chips and then tripping case-insensitive duplicate validation so users can’t save billing emails—guard restore solastis only reinserted when it isn’t already in the list. - In
packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx, Cmd/Ctrl+Z is currently captured even when there is nothing inremovedStack, which blocks normal text undo behavior in an empty input—only intercept the shortcut when a removed chip is actually available to restore.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx">
<violation number="1" location="packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx:40">
P2: Re-adding a removed address before undo creates a duplicate chip, so the form's case-insensitive duplicate validation blocks Save. Restore only when `last` is not already present.</violation>
<violation number="2" location="packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx:89">
P3: Cmd/Ctrl+Z on an empty input is swallowed even with no chip removal to restore, preventing normal draft-text undo. Intercept the shortcut only when `removedStack` has an entry.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| if (removedStack.length === 0) return; | ||
| const last = removedStack[removedStack.length - 1]!; | ||
| setRemovedStack((prev) => prev.slice(0, -1)); | ||
| setValue('emails', [...emails, last], { shouldDirty: true, shouldValidate: true }); |
There was a problem hiding this comment.
P2: Re-adding a removed address before undo creates a duplicate chip, so the form's case-insensitive duplicate validation blocks Save. Restore only when last is not already present.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx, line 40:
<comment>Re-adding a removed address before undo creates a duplicate chip, so the form's case-insensitive duplicate validation blocks Save. Restore only when `last` is not already present.</comment>
<file context>
@@ -19,10 +20,26 @@ export const InvoicingEmailsField: React.FC = () => {
+ if (removedStack.length === 0) return;
+ const last = removedStack[removedStack.length - 1]!;
+ setRemovedStack((prev) => prev.slice(0, -1));
+ setValue('emails', [...emails, last], { shouldDirty: true, shouldValidate: true });
+ };
+
</file context>
| setValue('emails', [...emails, last], { shouldDirty: true, shouldValidate: true }); | |
| setValue('emails', emails.some((email) => email.toLowerCase() === last.toLowerCase()) ? emails : [...emails, last], { shouldDirty: true, shouldValidate: true }); |
| // native undo handle an in-progress text edit in the draft input as usual. | ||
| const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'z'; | ||
| if (isUndo && !(e.target as HTMLInputElement).value) { | ||
| e.preventDefault(); |
There was a problem hiding this comment.
P3: Cmd/Ctrl+Z on an empty input is swallowed even with no chip removal to restore, preventing normal draft-text undo. Intercept the shortcut only when removedStack has an entry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx, line 89:
<comment>Cmd/Ctrl+Z on an empty input is swallowed even with no chip removal to restore, preventing normal draft-text undo. Intercept the shortcut only when `removedStack` has an entry.</comment>
<file context>
@@ -62,6 +79,15 @@ export const InvoicingEmailsField: React.FC = () => {
+ // native undo handle an in-progress text edit in the draft input as usual.
+ const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'z';
+ if (isUndo && !(e.target as HTMLInputElement).value) {
+ e.preventDefault();
+ undoLastRemoval();
}
</file context>
Problem
Customers manage their payment method and billing details (legal entity name, billing email, address, tax ID) in-app, but the section didn't match the approved design — it rendered as four separately-bordered cards under two subheadings instead of one unified panel — and only supported a single billing email, even though Orb (and the ticket) support multiple.
Solution
additionalEmailsonBillingInvoicingDetails, the Orb adapter/schema, the local noop billing client, and server-side validationInvoicingEmailsField) so a customer can add/remove multiple billing email addresses instead of typing one into a plain text fieldbrand/expMonth/expYear) onStripePaymentMethodso the payment-method row can show a brand + last4 + expiry summaryFixes NAN-6224
Testing
additionalEmails(Orb adapters,PUT /plans/billing/invoicing)