[PULSE-35] Billing tab: cancel and change-plan UI and copy #19

Merged
uz1mani merged 8 commits from staging into main 2026-02-09 14:16:21 +00:00
3 changed files with 383 additions and 117 deletions
Showing only changes of commit 29dd20a4a7 - Show all commits

View File

@@ -708,10 +708,10 @@ export default function OrganizationSettings() {
)}
{activeTab === 'billing' && (
<div className="space-y-12">
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Billing & Subscription</h2>
<p className="text-sm text-neutral-500 dark:text-neutral-400">Manage your subscription plan and payment methods.</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">Manage your plan, usage, and payment details.</p>
</div>
{isLoadingSubscription ? (
@@ -721,60 +721,82 @@ export default function OrganizationSettings() {
) : !subscription ? (
<div className="p-8 text-center bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-200 dark:border-neutral-800">
<p className="text-neutral-500">Could not load subscription details.</p>
<Button
variant="ghost"
onClick={loadSubscription}
className="mt-4"
>
Retry
</Button>
<Button variant="ghost" onClick={loadSubscription} className="mt-4">Retry</Button>
</div>
) : (
<div className="space-y-8">
{/* Current Plan */}
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<div className="flex items-start justify-between mb-6">
<div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-1">Current Plan</h3>
<div className="flex items-center gap-3">
<span className="text-2xl font-bold text-neutral-900 dark:text-white capitalize">
{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan
<div className="space-y-6">
{/* Trial notice */}
{subscription.subscription_status === 'trialing' && (
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/10 border border-yellow-200 dark:border-yellow-800 rounded-2xl flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex-1">
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Your free trial ends on{' '}
<span className="font-semibold">
{(() => {
const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null
return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }) : ''
})()}
</span>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
subscription.subscription_status === 'active'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: subscription.subscription_status === 'trialing'
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300'
}`}>
{subscription.subscription_status === 'trialing' ? 'Trial Active' : (subscription.subscription_status || 'Free')}
</span>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 items-start sm:items-center">
<Button
onClick={openChangePlanModal}
disabled={subscription.cancel_at_period_end}
>
Change plan
</Button>
{subscription.has_payment_method && (
<button
type="button"
onClick={handleManageSubscription}
disabled={isRedirectingToPortal}
className="text-sm text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 underline disabled:opacity-50"
>
Update payment method or view invoices
</button>
)}
</p>
<p className="text-xs text-yellow-700 dark:text-yellow-300 mt-0.5">
After the trial you'll be charged automatically unless you cancel before then.
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 pt-6 border-t border-neutral-200 dark:border-neutral-800">
{/* Cancel-at-period-end notice */}
{subscription.cancel_at_period_end && (
<div className="p-4 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded-2xl">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
Your subscription will end on{' '}
<span className="font-semibold">
{(() => {
const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null
return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }) : '—'
})()}
</span>
</p>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
You keep full access until then. Your data is retained for 30 days after. Use "Change plan" to resubscribe.
</p>
</div>
)}
{/* Plan & Usage card */}
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden">
{/* Plan header */}
<div className="p-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
<span className="text-xl font-bold text-neutral-900 dark:text-white capitalize">
{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan
</span>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
subscription.subscription_status === 'active'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: subscription.subscription_status === 'trialing'
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300'
}`}>
{subscription.subscription_status === 'trialing' ? 'Trial' : (subscription.subscription_status || 'Free')}
</span>
{subscription.billing_interval && (
<span className="text-xs text-neutral-500 capitalize">
Billed {subscription.billing_interval}ly
</span>
)}
</div>
<Button onClick={openChangePlanModal} disabled={subscription.cancel_at_period_end}>
Change plan
</Button>
</div>
{/* Usage stats */}
<div className="border-t border-neutral-200 dark:border-neutral-800 px-6 py-5 grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-6">
<div>
<div className="text-sm text-neutral-500 mb-1">Sites</div>
<div className="font-medium text-neutral-900 dark:text-white">
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Sites</div>
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
{typeof subscription.sites_count === 'number'
? subscription.plan_id === 'solo'
? `${subscription.sites_count} / 1`
@@ -783,109 +805,81 @@ export default function OrganizationSettings() {
</div>
greptile-apps[bot] commented 2026-02-09 13:54:45 +00:00 (Migrated from github.com)
Review

Invalid JSX mapping

The invoices branch renders invoices.map(...) as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 867:870

Comment:
**Invalid JSX mapping**

The invoices branch renders `invoices.map(...)` as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render.


How can I resolve this? If you propose a fix, please make it concise.
**Invalid JSX mapping** The invoices branch renders `invoices.map(...)` as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 867:870 Comment: **Invalid JSX mapping** The invoices branch renders `invoices.map(...)` as a bare token inside JSX. This needs to be wrapped in curly braces (or wrapped in a fragment) to compile/render. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-09 13:57:41 +00:00 (Migrated from github.com)
Review

Issue: The third branch of the ternary was invoices.map(...) without being wrapped in a JS expression. In JSX, that can be treated as a bare token instead of an expression, so the map result may not render correctly.
Fix: Wrapped the map in curly braces so the third branch is { invoices.map((invoice) => ( ... )) }, and adjusted the closing to ))} so the block is valid JSX.
Why: In JSX, only expressions inside { } are evaluated; wrapping the map in braces makes it a proper expression and keeps behavior consistent across tools and React versions.

Issue: The third branch of the ternary was invoices.map(...) without being wrapped in a JS expression. In JSX, that can be treated as a bare token instead of an expression, so the map result may not render correctly. Fix: Wrapped the map in curly braces so the third branch is { invoices.map((invoice) => ( ... )) }, and adjusted the closing to ))} so the block is valid JSX. Why: In JSX, only expressions inside { } are evaluated; wrapping the map in braces makes it a proper expression and keeps behavior consistent across tools and React versions.
</div>
<div>
<div className="text-sm text-neutral-500 mb-1">Pageviews this period</div>
<div className="font-medium text-neutral-900 dark:text-white">
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Pageviews</div>
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
greptile-apps[bot] commented 2026-02-09 14:00:51 +00:00 (Migrated from github.com)
Review

Hardcoded 'en-US' locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass undefined or the user's locale to respect their regional formatting preferences.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 875:875

Comment:
Hardcoded `'en-US'` locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass `undefined` or the user's locale to respect their regional formatting preferences.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.
Hardcoded `'en-US'` locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass `undefined` or the user's locale to respect their regional formatting preferences. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 875:875 Comment: Hardcoded `'en-US'` locale for currency formatting. This forces US-style formatting (e.g., $1,234.56) regardless of user's locale. Pass `undefined` or the user's locale to respect their regional formatting preferences. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-09 14:06:24 +00:00 (Migrated from github.com)
Review

No change: keeping the hardcoded 'en-US' locale for currency formatting.

No change: keeping the hardcoded 'en-US' locale for currency formatting.
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number'
? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}`
: '—'}
</div>
</div>
<div>
<div className="text-sm text-neutral-500 mb-1">Billing Interval</div>
<div className="font-medium text-neutral-900 dark:text-white capitalize">
{subscription.billing_interval ? `${subscription.billing_interval}ly` : ''}
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">
{subscription.subscription_status === 'trialing' ? 'Trial ends' : (subscription.cancel_at_period_end ? 'Access until' : 'Renews')}
</div>
</div>
<div>
<div className="text-sm text-neutral-500 mb-1">Pageview Limit</div>
<div className="font-medium text-neutral-900 dark:text-white">
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / month` : 'Unlimited'}
</div>
</div>
<div>
<div className="text-sm text-neutral-500 mb-1">
{subscription.subscription_status === 'trialing' ? 'Trial Ends On' : 'Renews On'}
</div>
<div className="font-medium text-neutral-900 dark:text-white">
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
{(() => {
const raw = subscription.current_period_end
const d = raw ? new Date(raw as string) : null
const ts = d ? d.getTime() : NaN
return raw && !Number.isNaN(ts) && ts !== 0 ? (d as Date).toLocaleDateString() : ''
const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null
return d && !Number.isNaN(d.getTime()) && d.getTime() !== 0 ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '—'
})()}
</div>
</div>
<div>
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Limit</div>
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / mo` : 'Unlimited'}
</div>
</div>
</div>
</div>
{/* Cancel-at-period-end notice or Cancel subscription action */}
{subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && (
<>
{subscription.cancel_at_period_end ? (
<div className="p-6 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded-2xl">
<h3 className="font-medium text-amber-800 dark:text-amber-200 mb-2">Subscription set to cancel</h3>
<p className="text-sm text-amber-700 dark:text-amber-300 mb-1">
Your subscription will end on{' '}
{(() => {
const raw = subscription.current_period_end
const d = raw ? new Date(raw as string) : null
return raw && d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString() : ''
})()}
. You can use the app until then.
</p>
<p className="text-xs text-amber-600 dark:text-amber-400">
Your data is retained for 30 days after access ends. You can resubscribe anytime using Change plan above.
</p>
</div>
) : (
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h3 className="font-medium text-neutral-900 dark:text-white mb-1">Cancel subscription</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
After cancellation, you can use the app until the end of your billing period. Your data is retained for 30 days after access ends.
</p>
</div>
<Button
variant="ghost"
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 border border-red-200 dark:border-red-800"
onClick={() => setShowCancelPrompt(true)}
>
Cancel subscription
</Button>
</div>
)}
</>
)}
{/* Quick actions */}
<div className="flex flex-wrap items-center gap-3">
{subscription.has_payment_method && (
<button
type="button"
onClick={handleManageSubscription}
disabled={isRedirectingToPortal}
className="inline-flex items-center gap-1.5 text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors disabled:opacity-50"
>
<ExternalLinkIcon className="w-4 h-4" />
Payment method & invoices
</button>
)}
{subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && !subscription.cancel_at_period_end && (
<button
type="button"
onClick={() => setShowCancelPrompt(true)}
className="inline-flex items-center gap-1.5 text-sm text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 transition-colors"
>
Cancel subscription
</button>
)}
</div>
{/* Invoice History */}
<div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-4">Invoice History</h3>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden">
{isLoadingInvoices ? (
<div className="flex items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" />
</div>
) : invoices.length === 0 ? (
<div className="p-8 text-center text-neutral-500">No invoices found.</div>
) : (
<div className="divide-y divide-neutral-200 dark:divide-neutral-800">
{invoices.map((invoice) => (
<div key={invoice.id} className="p-4 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-500">
<BookOpenIcon className="w-5 h-5" />
</div>
{invoices.length > 0 && (
<div>
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-3">Recent invoices</h3>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
{isLoadingInvoices ? (
<div className="flex items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" />
</div>
) : (
invoices.map((invoice) => (
<div key={invoice.id} className="px-4 py-3 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
<div className="flex items-center gap-3">
<div>
<div className="font-medium text-neutral-900 dark:text-white">
<span className="font-medium text-sm text-neutral-900 dark:text-white">
{(invoice.amount_paid / 100).toLocaleString('en-US', { style: 'currency', currency: invoice.currency.toUpperCase() })}
</div>
<div className="text-xs text-neutral-500">
{new Date(invoice.created * 1000).toLocaleDateString()}
</div>
</span>
<span className="text-xs text-neutral-500 ml-2">
{new Date(invoice.created * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
<div className="flex items-center gap-3">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium capitalize ${
invoice.status === 'paid'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: invoice.status === 'open'
@@ -895,34 +889,24 @@ export default function OrganizationSettings() {
{invoice.status}
greptile-apps[bot] commented 2026-02-09 13:48:48 +00:00 (Migrated from github.com)
Review

Invoices never render while loading
The invoices section is gated by invoices.length > 0 (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially []), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when isLoadingInvoices is true as well (or remove the outer length check).

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 859:906

Comment:
**Invoices never render while loading**
The invoices section is gated by `invoices.length > 0` (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially `[]`), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when `isLoadingInvoices` is true as well (or remove the outer length check).

How can I resolve this? If you propose a fix, please make it concise.
**Invoices never render while loading** The invoices section is gated by `invoices.length > 0` (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially `[]`), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when `isLoadingInvoices` is true as well (or remove the outer length check). <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 859:906 Comment: **Invoices never render while loading** The invoices section is gated by `invoices.length > 0` (components/settings/OrganizationSettings.tsx:860), so when invoices are still loading (initially `[]`), the entire UI (including the spinner) is not rendered. This guarantees no loading state is shown and also hides the “No invoices found” state entirely. You likely want to render the section when `isLoadingInvoices` is true as well (or remove the outer length check). How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-09 13:52:22 +00:00 (Migrated from github.com)
Review

Issue: The invoices block was wrapped in invoices.length > 0, so while invoices was still [] (e.g. during load) the whole block (including spinner and empty state) was hidden.
Fix: The “Recent invoices” block always renders. The inner content is a three-way branch: show spinner when isLoadingInvoices, “No invoices found.” when loaded and invoices.length === 0, otherwise the list. The outer invoices.length > 0 guard was removed.
Why: Users need to see a loading state while invoices are fetched and an explicit empty state when there are none, not a missing section.

Issue: The invoices block was wrapped in invoices.length > 0, so while invoices was still [] (e.g. during load) the whole block (including spinner and empty state) was hidden. Fix: The “Recent invoices” block always renders. The inner content is a three-way branch: show spinner when isLoadingInvoices, “No invoices found.” when loaded and invoices.length === 0, otherwise the list. The outer invoices.length > 0 guard was removed. Why: Users need to see a loading state while invoices are fetched and an explicit empty state when there are none, not a missing section.
</span>
{invoice.invoice_pdf && (
<a
href={invoice.invoice_pdf}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-neutral-500 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
title="Download PDF"
>
<DownloadIcon className="w-5 h-5" />
<a href={invoice.invoice_pdf} target="_blank" rel="noopener noreferrer"
className="p-1.5 text-neutral-400 hover:text-neutral-900 dark:hover:text-white rounded-lg transition-colors" title="Download PDF">
<DownloadIcon className="w-4 h-4" />
greptile-apps[bot] commented 2026-02-09 14:09:56 +00:00 (Migrated from github.com)
Review

JSX syntax error: opening brace without matching close

Line 874 has { before invoices.map(...) but the closing is just ) instead of )}. This will cause a compilation error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 874:911

Comment:
JSX syntax error: opening brace without matching close

Line 874 has `{` before `invoices.map(...)` but the closing is just `)` instead of `)}`. This will cause a compilation error.

How can I resolve this? If you propose a fix, please make it concise.
JSX syntax error: opening brace without matching close Line 874 has `{` before `invoices.map(...)` but the closing is just `)` instead of `)}`. This will cause a compilation error. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 874:911 Comment: JSX syntax error: opening brace without matching close Line 874 has `{` before `invoices.map(...)` but the closing is just `)` instead of `)}`. This will cause a compilation error. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-09 14:13:18 +00:00 (Migrated from github.com)
Review

Issue: The third branch of the ternary was ( { invoices.map(...) } ). The { before invoices.map was closed by } on the same line as )), and the ) that closes the ternary branch was on the next line. That layout could be parsed as a missing )} and trigger a JSX syntax/compilation error.
Fix: Wrapped the third branch in a fragment so the structure is a single expression:
( <> { invoices.map((invoice) => ( ... )) } </> ).
The { ... } block is clearly closed by )}, and the ternary branch is closed by ) after </>.
Why: Using a fragment makes the ternary’s third branch one JSX node and keeps the expression’s {/} pairing obvious, which avoids the reported syntax/compilation issue.

Issue: The third branch of the ternary was ( { invoices.map(...) } ). The { before invoices.map was closed by } on the same line as )), and the ) that closes the ternary branch was on the next line. That layout could be parsed as a missing )} and trigger a JSX syntax/compilation error. Fix: Wrapped the third branch in a fragment so the structure is a single expression: ( <> { invoices.map((invoice) => ( ... )) } </> ). The { ... } block is clearly closed by )}, and the ternary branch is closed by ) after </>. Why: Using a fragment makes the ternary’s third branch one JSX node and keeps the expression’s {/} pairing obvious, which avoids the reported syntax/compilation issue.
</a>
)}
{invoice.hosted_invoice_url && (
<a
href={invoice.hosted_invoice_url}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-neutral-500 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
title="View Invoice"
>
<ExternalLinkIcon className="w-5 h-5" />
<a href={invoice.hosted_invoice_url} target="_blank" rel="noopener noreferrer"
className="p-1.5 text-neutral-400 hover:text-neutral-900 dark:hover:text-white rounded-lg transition-colors" title="View invoice">
<ExternalLinkIcon className="w-4 h-4" />
</a>
)}
</div>
</div>
))}
</div>
)}
))
)}
</div>
</div>
</div>
)}
</div>
)}
</div>