Skip to content

fix(billing): send externally billed orgs to their provider's invoices - #107953

Open
MattBro wants to merge 4 commits into
masterfrom
matt/vercel-open-invoice-link
Open

MattBro wants to merge 4 commits into
masterfrom
matt/vercel-open-invoice-link

Conversation

@MattBro

@MattBro MattBro commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • Orgs billed through an external provider, such as the Vercel Marketplace, pay their invoices through that provider. The billing page still showed their members Stripe's hosted invoice page and portal, where they could pay the same invoice a second time.
  • The open-invoice banner was the most direct path. A provider-billed invoice stays open in Stripe until the provider reports payment, so the banner showed a "View invoice" button that opened Stripe's pay page.

Changes

  • An org counts as externally billed when billing.billing_provider is anything other than posthog, or when a provider invoices URL is set. The billing API starts sending billing_provider in a companion change. Until then, the URL check covers Vercel orgs.
  • Externally billed orgs never get a Stripe payment link:
    • With a provider invoices page, the banner, unsubscribe errors, portal button and credit links go there and say "View invoices".
    • Without one, the banner, portal button and invoice links are hidden, and unsubscribe errors keep their text without a link.
  • The banner is now a selector over the open invoices and billing, and shows once both have loaded. When invoices finish first, the page makes one /api/billing request instead of two.
  • The credit purchase dialog tells externally billed orgs that their provider collects the payment, instead of saying their card will be charged.
  • Orgs that PostHog bills directly see no change.
  • One behavior change to review: open invoices no longer disable add-on buttons for an externally billed org with no invoices page. Those invoices stay open until the provider pays.

How did you test this code?

  • billingLogic.test.ts tables cover direct billing, a provider with an invoices page, and a provider without one, for the banner and the unsubscribe links.
  • The race row holds /api/billing open until invoices load, then asserts a single request. The previous version of this PR made two.
  • The new StripePortalButton.test.tsx covers the portal link, the provider link, and rendering nothing.
  • The new rows fail against the previous commit on this branch and pass with this one. Test URLs are invented.
  • Not checked in a browser. The local stack has no provider-billed org to render.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None. The billing page behavior for provider-billed orgs has no docs page. Labeled skip-inkeep-docs.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5), including the subagents that wrote and reviewed the change.

  • Came out of an org paying one invoice twice through the banner. The tests use invented values, and nothing from that case is in the diff.
  • A duplicate search (external billing provider invoices, stripe_portal_url vercel billing) found no other open PR.
  • Skills: /writing-pr-descriptions (repo), plus the author's own PR-readiness command.
  • CodeRabbit CLI pass skipped: the CLI is signed out on this machine. The CodeRabbit bot reviews the PR here.
  • The first commit keyed the rule on the Vercel invoices URL alone, and waited for billing inside the invoices loader. The rule widened to any external provider. The wait became a selector after a review showed it doubled the /api/billing request when invoices loaded first.

🤖 Generated with Claude Code

Organizations billed through an external provider such as the Vercel
Marketplace pay through that provider. The billing page still linked their
open-invoice banner, unsubscribe errors and credit dialogs to Stripe's hosted
invoice page or portal, where they could pay the same invoice a second time.
Link to external_billing_provider_invoices_url instead whenever it is set, and
wait for billing to load before building the open-invoice banner so an early
invoices response can't fall back to the Stripe link.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Sep 28, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

✅ Trunk lane — non-backend lane

This PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes.

⚠️ Complexity (TypeScript) — 6 functions above the limit (max 23)

Cyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions.

Function Location Complexity Limit
UnsubscribeSurveyModal frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx:40 23 10
determineBillingAlert frontend/src/scenes/billing/billingLogic.tsx:1481 22 10
CreditCTAHero frontend/src/scenes/billing/CreditCTAHero.tsx:16 17 10
deactivateProduct frontend/src/scenes/billing/billingLogic.tsx:881 14 10
registerInstrumentationProps frontend/src/scenes/billing/billingLogic.tsx:1661 14 10
<anonymous> frontend/src/scenes/billing/billingLogic.tsx:1253 11 10
✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

⚠️ Bundle size — 🔺 +6.9 KiB (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 68.86 MiB · 🔺 +6.9 KiB (+0.0%)

File Size Δ vs base
render-query/src/render-query/render-query.js 20.15 MiB 🔺 +1.9 KiB (+0.0%)
posthog-app/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistScene.js 22.4 KiB 🔺 +1.4 KiB (+6.8%)
posthog-app/_parent/products/workflows/frontend/Broadcasts/BroadcastScene.js 63.6 KiB 🔺 +1.3 KiB (+2.1%)
posthog-app/_parent/products/mcp_analytics/frontend/MCPAnalyticsScene.js 180.9 KiB 🔺 +1.1 KiB (+0.6%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

✅ Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.57 MiB · 22 files 🔺 +88 B (+0.0%) █████████░ 85.5% of 1.84 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
3.51 MiB · 629 files 🔺 +1.1 KiB (+0.0%) █████████░ 87.2% of 4.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.33 MiB · 2,332 files 🔺 +1.9 KiB (+0.0%) █████████░ 87.9% of 8.34 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/index.tsx
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
854 B src/scenes/ChunkLoadErrorBoundary.tsx
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.14_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
216.0 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
100.4 KiB src/lib/api.ts
88.4 KiB src/products.tsx
69.4 KiB src/lib/lemon-ui/icons/icons.tsx
40.1 KiB src/lib/utils/eventUsageLogic.ts
38.7 KiB ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js
33.9 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
28.4 KiB src/scenes/scenes.ts
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.14_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
271.7 KiB src/taxonomy/core-filter-definitions-by-group.json
216.0 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
100.4 KiB src/lib/api.ts
98.5 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
88.4 KiB src/products.tsx

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

✅ Toolbar bundle — eager 2.16 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.16 MiB · 19 files 🔺 +88 B (+0.0%) ████░░░░░░ 37.7% of 5.72 MiB
Deferred (lazy) 2.10 MiB · 44 files no change n/a — loads on demand
Loader dist/toolbar.js 1.2 KiB no change █░░░░░░░░░ 6.0% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
800.3 KiB dist/toolbar/toolbar-app-BB53FERC.css
651.5 KiB dist/toolbar/chunk-chunk-I5EINXHJ.js
259.4 KiB dist/toolbar/chunk-chunk-FDSGHPDO.js
138.3 KiB dist/toolbar/chunk-chunk-52D5E4WE.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
75.2 KiB dist/toolbar/toolbar-app-37VQQU5C.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-UCXWWLV3.js
21.0 KiB dist/toolbar/chunk-chunk-IH7RHM2D.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

✅ Dist folder size — 🔺 +136.1 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 945.99 MiB · 🔺 +136.1 KiB (+0.0%)

✅ Playwright — all passed

All tests passed.

View test results →

@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[Critical risk] Routes externally billed organizations to their provider's invoice pages.

The PR appears safe to merge based on the changes reviewed.

Reviews (2) · Last reviewed commit: "fix(billing): hide Stripe payment links ..."

Comment thread frontend/src/scenes/billing/CreditCTAHero.tsx Outdated
Comment thread frontend/src/scenes/billing/billingLogic.tsx Outdated
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: c80ec962-65f6-438d-8ead-bcf70b38e830

📥 Commits

Reviewing files that changed from the base of the PR and between 05dfbfa and 9d454b4.

📒 Files selected for processing (2)
  • frontend/src/scenes/billing/StripePortalButton.test.tsx
  • frontend/src/scenes/billing/StripePortalButton.tsx

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Billing logic now stores open-invoice data and derives invoice warnings from billing state. Invoice destinations depend on the billing provider and available URLs. Billing UI links use the selected destination and are omitted when no URL is available. Credit-purchase confirmations also use billing-provider-specific destinations. Tests cover invoice loading order, open-invoice warnings, unsubscribe errors, and portal-button destinations.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 9d454

Externally billed organizations use their provider’s invoice page when configured, while direct billing retains its existing invoice destinations. No actionable merge-blocking risk is evident in the supplied implementation and test coverage.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 9d454

Provider-billed organizations are less likely to be sent to Stripe to pay twice. The remaining risk is that an organization with open invoices but no provider invoices page loses the warning associated with adding subscription items. Whether the billing service independently enforces that restriction is not established.

Retained concerns

  • Medium · security · inferred: For an externally billed organization without an invoices URL, open invoices no longer produce the warning used by the billing UI; the PR also identifies that add-on buttons become available in this state. If the service relies on that UI restriction, unpaid invoices may no longer block subscription additions. Server-side enforcement has not been established.
Security review details

Security Blast Radius

  • inferred — The affected scope is organization billing UI for provider-billed accounts, particularly accounts with open invoices and no provider invoices page. The evidence does not establish an anonymous entrypoint, a changed service permission, or cross-organization access.

Security Findings and Attack Paths

  • inferred — If add-on eligibility is enforced only through the warning-dependent UI restriction, a member permitted to manage billing could attempt additions while provider-paid invoices remain open. The available evidence does not show that the billing API accepts such an addition, so this is a conditional control gap, not a verified exploit.

Trust Boundaries and Controls

  • inferred — Provider and URL fields cross from the billing API response into external-navigation targets without frontend destination validation shown here. Attacker control of those fields is not established; a known external provider with no URL instead suppresses the Stripe link.

Resilience and Maintainability Implications

  • inferred — An invoice-fetch failure yields no open-invoice warning, while concurrent billing-response ordering is unproven. These states matter if the warning is expected to carry a billing-control guarantee rather than provide guidance alone.

Hardening Proposals

  • proposed — Confirm that the billing service authorizes provider URLs for the current organization and enforces any required open-invoice restriction on add-on changes independently of whether the frontend can show a payment link.
  • proposed — Coordinate provider-field availability with the frontend rollout, including provider-billed accounts that have no invoices URL, so an omitted classification cannot be interpreted as direct billing.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and explains the problem, user-visible changes, test coverage, release status, documentation status, and agent context. It omits screenshots for the frontend changes and a …
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
frontend/src/scenes/billing/CreditCTAHero.tsx-139-139 (1)

139-139: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the selected invoice URL for both rendering and navigation.

If a pending credit overview has invoice_url: null while billing.external_billing_provider_invoices_url is set, the current guard and click handler both block the external destination. Derive the fallback URL once, then use it for the button condition and window.open.

Suggested fix
+    const invoiceUrl = billing?.external_billing_provider_invoices_url || creditOverview.invoice_url
+
     return (
...
-                        {creditOverview.status === 'pending' && creditOverview.invoice_url && (
+                        {creditOverview.status === 'pending' && invoiceUrl && (
                             <LemonButton
                                 type="primary"
-                                onClick={() =>
-                                    creditOverview.invoice_url &&
-                                    window.open(
-                                        billing?.external_billing_provider_invoices_url || creditOverview.invoice_url,
-                                        '_blank'
-                                    )
-                                }
+                                onClick={() => window.open(invoiceUrl, '_blank')}
frontend/src/scenes/billing/billingLogic.tsx-1002-1003 (1)

1002-1003: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve the open-invoice warning when billing loading fails.

When open invoices load before billing, loadInvoices awaits asyncActions.loadBilling() inside the same try block as the invoice request. A billing request failure rejects this async action, reaches the outer catch, and returns null. This removes the warning even though the invoice response already contains open invoices.

Handle billing-load failure separately so the warning can still use the hosted-invoice link.

🐛 Suggested fix
                             if (!values.billing) {
-                                await asyncActions.loadBilling()
+                                try {
+                                    await asyncActions.loadBilling()
+                                } catch (error) {
+                                    console.error(error)
+                                }
                             }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 622be79a-a54d-4feb-ab6e-460323243502

📥 Commits

Reviewing files that changed from the base of the PR and between 40eeb71 and b76449e.

📒 Files selected for processing (4)
  • frontend/src/scenes/billing/CreditCTAHero.tsx
  • frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
  • frontend/src/scenes/billing/billingLogic.test.ts
  • frontend/src/scenes/billing/billingLogic.tsx

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

@posthog

posthog Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

Install the Visual Review Chrome extension to see visual review results at the top of your pull requests.

@trunk-io

trunk-io Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

Apply the rule to any org billed through an external provider, using
billing_provider when the billing API sends it and the provider invoices URL
otherwise. An externally billed org with no invoices page gets no Stripe link,
no open-invoice banner and no portal button. Build the banner in a selector
over the open invoices and billing, so the page no longer requests billing a
second time when invoices load first, and label provider links "View invoices".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@MattBro MattBro added the skip-inkeep-docs Use this label to skip an Inkeep docs PR in posthog.com label Sep 28, 2026
@MattBro
MattBro marked this pull request as ready for review September 28, 2026 20:13
@MattBro
MattBro requested a review from a team September 28, 2026 20:13
@github-actions
github-actions Bot requested a deployment to preview-pr-107953 September 28, 2026 20:13 In progress
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 9d454b4 · box box-5bc6661b198c · ready in 812s (push → usable) · build log · rebuilds on every push, torn down on close

The pending credit button needed a Stripe hosted invoice even when it opens
the provider's invoices page, so it could stay hidden. The credit dialog told
externally billed orgs that their card would be charged and linked "card on
file" to the provider's invoices. It now says the provider collects the payment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
frontend/src/scenes/billing/StripePortalButton.tsx-8-17 (1)

8-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope the customer_id guard to direct PostHog billing.

The billing API permits a missing or null customer_id and adds external_billing_provider_invoices_url independently for Vercel integrations. StripePortalButton returns before it selects that URL, so externally billed organizations can lose their invoice link. Keep the guard for direct PostHog billing.

Suggested fix
-    if (!billing?.customer_id) {
+    if (!billing || (!isExternallyBilled &amp;&amp; !billing.customer_id)) {
        return null
    }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 8d1e273d-f6d3-4a57-a8c7-6c29f130d53f

📥 Commits

Reviewing files that changed from the base of the PR and between b76449e and 05dfbfa.

📒 Files selected for processing (7)
  • frontend/src/scenes/billing/CreditCTAHero.tsx
  • frontend/src/scenes/billing/StripePortalButton.test.tsx
  • frontend/src/scenes/billing/StripePortalButton.tsx
  • frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
  • frontend/src/scenes/billing/billingLogic.test.ts
  • frontend/src/scenes/billing/billingLogic.tsx
  • frontend/src/types.ts

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.

The portal button returned nothing when billing had no customer id, before it
looked at the provider invoices URL. Keep that check for direct billing only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
preview-pr-107953 — 9d454b4b Deployed Sep 28, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-inkeep-docs Use this label to skip an Inkeep docs PR in posthog.com

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant