Skip to content

feat(webapp): let customers manage payment and billing details in-app - #6908

Draft
macko911 wants to merge 10 commits into
masterfrom
matej/nan-6224-let-customers-manage-payment-and-billing-details-in-app-with
Draft

feat(webapp): let customers manage payment and billing details in-app#6908
macko911 wants to merge 10 commits into
masterfrom
matej/nan-6224-let-customers-manage-payment-and-billing-details-in-app-with

Conversation

@macko911

Copy link
Copy Markdown
Contributor

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

  • Support multiple billing emails end to end: additionalEmails on BillingInvoicingDetails, the Orb adapter/schema, the local noop billing client, and server-side validation
  • Add a chip input (InvoicingEmailsField) so a customer can add/remove multiple billing email addresses instead of typing one into a plain text field
  • Collapse the "Billing information" section from four bordered cards into one panel with internal dividers, matching the approved design — title + "View all invoices" link, payment method, legal entity/email fields, collapsible billing address and tax ID rows, then save
  • Expose Stripe's card brand and expiry (brand/expMonth/expYear) on StripePaymentMethod so the payment-method row can show a brand + last4 + expiry summary

Fixes NAN-6224

Testing

  • Added/updated backend unit and integration tests for additionalEmails (Orb adapters, PUT /plans/billing/invoicing)
  • Verified locally in the browser: panel layout matches the Figma design, adding/removing email chips works, invalid addresses are rejected with an inline error, and billing address/tax ID expand-collapse still works within the single panel

@linear-code

linear-code Bot commented Jul 24, 2026

Copy link
Copy Markdown

NAN-6224

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Preview Deploy

Status URL Deploy Logs Last Updated
✅ Ready Preview URL Deploy Logs 24 Jul 2026, 13:31 UTC

@macko911 macko911 changed the title Unify billing information panel and support multiple billing emails feat(webapp): let customers manage payment and billing details in-app Jul 24, 2026
macko911 added 3 commits July 24, 2026 12:36
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.
@macko911
macko911 force-pushed the matej/nan-6224-let-customers-manage-payment-and-billing-details-in-app-with branch from 738a360 to 40aa67c Compare July 24, 2026 10:37

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/types/lib/billing/types.ts
Comment thread packages/server/lib/controllers/v1/plans/billing/putInvoicingDetails.ts Outdated
Comment thread packages/webapp/src/pages/Team/Billing/components/InvoicingDetailsForm.tsx Outdated
Comment thread packages/server/lib/controllers/v1/plans/billing/putInvoicingDetails.ts Outdated
Comment thread packages/webapp/src/pages/Team/Billing/components/Payment.tsx Outdated
Comment thread packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx Outdated
macko911 added 3 commits July 24, 2026 13:12
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.
macko911 added 2 commits July 24, 2026 15:26
- 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 so last is 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 in removedStack, 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 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
Suggested change
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant