Skip to content

fix(site): add currency conversion to pricing page#7752

Merged
mhartington merged 7 commits intomainfrom
pricing-conversion-page-fix
Apr 2, 2026
Merged

fix(site): add currency conversion to pricing page#7752
mhartington merged 7 commits intomainfrom
pricing-conversion-page-fix

Conversation

@ArthurGamby
Copy link
Copy Markdown
Contributor

@ArthurGamby ArthurGamby commented Apr 2, 2026

Summary

  • Previously the currency selector only swapped the symbol without converting the amount ($49 → €49)
  • Added hardcoded exchange rates for all 10 supported currencies with a convertFromUsd() helper
  • Plan prices now show rounded whole numbers for clean display (e.g. €45, £39, ¥7,399)
  • Calculator totals and breakdown line items also convert correctly
  • Per-unit micro-prices (per 1,000 ops, per GB) keep decimal precision

Test plan

  • Visit /pricing and switch through all 10 currencies
  • Verify hero card plan prices are rounded whole numbers
  • Verify plan bullet points (per 1,000 ops, per GB) show converted micro-prices
  • Verify calculator totals and breakdown line items convert
  • Verify JPY/KRW show no unnecessary decimals
  • Verify USD still displays correctly (rate = 1)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Pricing calculator now converts USD to the selected currency with standardized formatting (rounded for larger values, preserved micro-decimals for tiny values, improved compact display) and small layout/markup refinements for clearer presentation.
  • New Features
    • Added a pricing comparison table and a “Compare plans” section on the pricing page.
  • Bug Fixes / Cleanup
    • Renamed footer link from “Optimize” to “Accelerate” and cleaned up footer/share link formatting.

Previously the currency selector only swapped the symbol without
converting the amount. Now applies approximate exchange rates so
prices display realistic converted values with clean rounding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blog Ready Ready Preview, Comment Apr 2, 2026 3:26pm
docs Ready Ready Preview, Comment Apr 2, 2026 3:26pm
eclipse Ready Ready Preview, Comment Apr 2, 2026 3:26pm
site Ready Ready Preview, Comment Apr 2, 2026 3:26pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a19b8d7b-2b81-4c7b-bc2b-81973801d9d7

📥 Commits

Reviewing files that changed from the base of the PR and between b520209 and 7d71351.

📒 Files selected for processing (5)
  • apps/site/src/app/pricing/page.tsx
  • apps/site/src/app/pricing/pricing-calculator.tsx
  • apps/site/src/app/pricing/pricing-comparison-table.tsx
  • apps/site/src/app/pricing/pricing-hero-plans.tsx
  • packages/ui/src/data/footer.ts

Walkthrough

Pricing now converts USD amounts to the selected currency before formatting, adds exchange rates and per-currency config, introduces a client-only pricing comparison table component, reinserts the comparison UI into pricing page content, and applies minor JSX/className/footer wording tweaks.

Changes

Cohort / File(s) Summary
Currency infra & data
apps/site/src/app/pricing/pricing-data.ts
Added exchangeRates, currencyConfig, and convertFromUsd(amountUsd, currency); changed formatAmountForAllCurrencies to accept USD and apply per-currency decimals/micro-decimal rules; converted comparison rows to typed ComparisonCell objects.
Calculator updates
apps/site/src/app/pricing/pricing-calculator.tsx
Formatting helpers now take valueUsd and call convertFromUsd before rounding/formatting; several arithmetic expressions parenthesized; JSX and prop formatting reflowed—no behavioral API changes.
Comparison UI component
apps/site/src/app/pricing/pricing-comparison-table.tsx
New client-only PricingComparisonTable({ currency }) component rendering comparisonSections into a fixed-layout table and injecting per-currency formatted prices; adds Badge/table usage for plan columns.
Pricing page integration
apps/site/src/app/pricing/pricing-page-content.tsx, apps/site/src/app/pricing/page.tsx
PricingPageContent now renders the PricingComparisonTable; page.tsx removes the old inline compare-table block and simplifies related imports/JSX.
Hero & Footer tweaks
apps/site/src/app/pricing/pricing-hero-plans.tsx, packages/ui/src/data/footer.ts
Minor JSX condensation and className adjustment in hero plans; footer link text/url changed from “Optimize” → “Accelerate”; simplified share URL callback parameter typing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective of the pull request: adding currency conversion functionality to the pricing page. It directly reflects the primary change across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@argos-ci
Copy link
Copy Markdown

argos-ci bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Apr 2, 2026, 3:30 PM

Use the same rates as the old website (lib/currency.ts) for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/site/src/app/pricing/pricing-calculator.tsx (1)

254-257: Render description as text instead of injecting HTML.

getPlanDescription() currently returns plain text, so dangerouslySetInnerHTML buys nothing here and leaves an unnecessary HTML-injection footgun in the card component. Rendering {description} keeps the same UI while restoring React escaping.

Small cleanup
-          <p
-            className="m-0 max-w-[277px] text-xs leading-4 text-foreground-neutral-weaker"
-            dangerouslySetInnerHTML={{ __html: description }}
-          />
+          <p className="m-0 max-w-[277px] text-xs leading-4 text-foreground-neutral-weaker">
+            {description}
+          </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/pricing/pricing-calculator.tsx` around lines 254 - 257, The
component is using dangerouslySetInnerHTML to render description even though
getPlanDescription() returns plain text; replace the <p> that uses
dangerouslySetInnerHTML with a normal JSX text render of the description (i.e.,
render {description}) to restore React escaping and remove the HTML-injection
risk; update the JSX in pricing-calculator.tsx where description is used (and
referenced by getPlanDescription()) to output the variable directly instead of
using dangerouslySetInnerHTML.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/app/pricing/pricing-calculator.tsx`:
- Around line 77-84: The current formatCurrency function (formatCurrency)
improperly rounds any converted value >= 1 to an integer; change behavior so
this helper formats "actual amounts" using the currency's configured decimals
(use currencyConfig[currency].decimals) and remove the converted >= 1 integer
rounding there; then add a separate "headline" formatter (e.g.,
formatHeadlinePrice or formatCurrencyHeadline) that applies the integer rounding
for display of plan headlines. Update uses of formatCurrency in calculated
breakdowns to call the actual-amount formatter and only use the new headline
formatter for top-level plan prices; keep references to convertFromUsd and
symbols[currency] when building both formatters.

In `@apps/site/src/app/pricing/pricing-data.ts`:
- Around line 71-89: formatAmountForAllCurrencies() is currently forcing
per-unit prices through the same whole-rounding path (using Math.round and
digits>config.decimals) which causes wrong displays; change the logic to keep
per-unit precision by using the converted value (from convertFromUsd) rather
than Math.round except for the headline-only rounding path, compute
effectiveFractionDigits as: if isMicroPrice then config.microDecimals else if
Number.isInteger(converted) then 0 else Math.min(digits, config.decimals);
format displayValue using the original converted number (not Math.round) with
toLocaleString and those effectiveFractionDigits so integer converted amounts
drop “.00” while non-integers keep necessary precision; update references in
this function (symbols loop, typedCode, converted, config, isMicroPrice,
effectiveDigits, displayValue) accordingly.

---

Nitpick comments:
In `@apps/site/src/app/pricing/pricing-calculator.tsx`:
- Around line 254-257: The component is using dangerouslySetInnerHTML to render
description even though getPlanDescription() returns plain text; replace the <p>
that uses dangerouslySetInnerHTML with a normal JSX text render of the
description (i.e., render {description}) to restore React escaping and remove
the HTML-injection risk; update the JSX in pricing-calculator.tsx where
description is used (and referenced by getPlanDescription()) to output the
variable directly instead of using dangerouslySetInnerHTML.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2292cd39-dc5e-46a3-820a-399a17fda8d2

📥 Commits

Reviewing files that changed from the base of the PR and between 89a4c9a and 778a094.

📒 Files selected for processing (2)
  • apps/site/src/app/pricing/pricing-calculator.tsx
  • apps/site/src/app/pricing/pricing-data.ts

mhartington
mhartington previously approved these changes Apr 2, 2026
Moved comparison table into a client component so it receives the
currency state. Cache tag invalidation prices now convert with the
selected currency instead of showing hardcoded USD values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add top margin to Pro card on mobile so POPULAR badge doesn't
  overlap the card above it
- Make comparison table horizontally scrollable on mobile with
  min-width to prevent column squishing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/site/src/app/pricing/pricing-data.ts (1)

71-85: ⚠️ Potential issue | 🟠 Major

Per-unit prices are still being whole-rounded.

Line 79 still routes any digits <= config.decimals value through Math.round(). That breaks /GB prices for 2-decimal currencies and still leaves .00 on integer JPY/KRW conversions. Only the headline card price should whole-round.

Suggested fix
 export function formatAmountForAllCurrencies(amountUsd: number, digits: number): CurrencyMap {
   return Object.fromEntries(
     Object.entries(symbols).map(([code, symbol]) => {
       const typedCode = code as Symbol;
       const converted = convertFromUsd(amountUsd, typedCode);
       const config = currencyConfig[typedCode];
-      const isMicroPrice = digits > config.decimals;
-      const effectiveDigits = isMicroPrice ? config.microDecimals : digits;
-      const displayValue = isMicroPrice ? converted : Math.round(converted);
+      const isHeadlinePrice = digits === 0;
+      const isMicroPrice = digits > config.decimals;
+      const effectiveDigits = isHeadlinePrice
+        ? 0
+        : Number.isInteger(converted)
+          ? 0
+          : isMicroPrice
+            ? config.microDecimals
+            : Math.min(digits, config.decimals);
+      const displayValue = isHeadlinePrice ? Math.round(converted) : converted;
       return [
         code,
         `${symbol}${displayValue.toLocaleString("en-US", {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/pricing/pricing-data.ts` around lines 71 - 85, The
formatAmountForAllCurrencies function is improperly whole-rounding values by
using Math.round whenever digits <= config.decimals; remove the Math.round and
always format the raw converted value with toLocaleString using the computed
effectiveDigits (where effectiveDigits = config.microDecimals when digits >
config.decimals, otherwise digits) so per-unit prices retain their decimal
places (leave any whole-rounding behavior to the headline-card-specific code
instead); update the displayValue logic in formatAmountForAllCurrencies (which
calls convertFromUsd and reads currencyConfig and symbols) to pass the unrounded
converted value into toLocaleString with the correct effectiveDigits.
🧹 Nitpick comments (1)
apps/site/src/app/pricing/pricing-comparison-table.tsx (1)

28-56: Derive the plan columns from shared pricing data instead of hardcoding them.

["Free", "Starter", "Pro", "Business"] and colSpan={5} duplicate data that already exists in pricing-data.ts. Any plan rename, reorder, or count change will desync this table from the pricing source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/pricing/pricing-comparison-table.tsx` around lines 28 - 56,
Replace the hardcoded plan list and fixed column span with the canonical plans
array exported from pricing-data.ts: import the exported plans identifier from
pricing-data.ts and use plans.map(...) in place of
["Free","Starter","Pro","Business"] when rendering TableHead (same key/Badge
logic), and change the section TableCell colSpan from the hardcoded 5 to compute
dynamically as plans.length + 1 (to account for the feature column). Also ensure
any other uses of the hardcoded labels or the literal 5 in this component are
replaced to derive from that same exported plans array so renames/reorders/count
changes stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/app/pricing/pricing-comparison-table.tsx`:
- Around line 25-27: The TableHead currently uses comparisonSections[0]?.title
which duplicates the first row and changes when sections are reordered; replace
that dynamic header with a static label (e.g., "Feature") so the top-left cell
is a fixed column heading. Update the TableHead element in
pricing-comparison-table.tsx (the TableHead JSX that currently references
comparisonSections[0]?.title) to render the static string and leave the section
rows to render their own titles from comparisonSections.

In `@packages/ui/src/data/footer.ts`:
- Around line 190-191: The share URL helpers currently interpolate raw values
and must encode query params: update the url properties for the LinkedIn, X
(Twitter), and Bluesky helpers so they call encodeURIComponent(...) on all
interpolated user values — for LinkedIn encode current_page; for the X helper
encode text_data, current_page, and hashtags; for the Bluesky helper encode
text_data and current_page — ensure you replace direct ${...} interpolation
inside the url strings with encoded values in the respective url functions.

---

Duplicate comments:
In `@apps/site/src/app/pricing/pricing-data.ts`:
- Around line 71-85: The formatAmountForAllCurrencies function is improperly
whole-rounding values by using Math.round whenever digits <= config.decimals;
remove the Math.round and always format the raw converted value with
toLocaleString using the computed effectiveDigits (where effectiveDigits =
config.microDecimals when digits > config.decimals, otherwise digits) so
per-unit prices retain their decimal places (leave any whole-rounding behavior
to the headline-card-specific code instead); update the displayValue logic in
formatAmountForAllCurrencies (which calls convertFromUsd and reads
currencyConfig and symbols) to pass the unrounded converted value into
toLocaleString with the correct effectiveDigits.

---

Nitpick comments:
In `@apps/site/src/app/pricing/pricing-comparison-table.tsx`:
- Around line 28-56: Replace the hardcoded plan list and fixed column span with
the canonical plans array exported from pricing-data.ts: import the exported
plans identifier from pricing-data.ts and use plans.map(...) in place of
["Free","Starter","Pro","Business"] when rendering TableHead (same key/Badge
logic), and change the section TableCell colSpan from the hardcoded 5 to compute
dynamically as plans.length + 1 (to account for the feature column). Also ensure
any other uses of the hardcoded labels or the literal 5 in this component are
replaced to derive from that same exported plans array so renames/reorders/count
changes stay in sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3ad2e94-2f26-4334-9142-e2babd8aaf21

📥 Commits

Reviewing files that changed from the base of the PR and between 5055b66 and b520209.

📒 Files selected for processing (5)
  • apps/site/src/app/pricing/page.tsx
  • apps/site/src/app/pricing/pricing-comparison-table.tsx
  • apps/site/src/app/pricing/pricing-data.ts
  • apps/site/src/app/pricing/pricing-page-content.tsx
  • packages/ui/src/data/footer.ts

Comment on lines +25 to +27
<TableHead className="bg-background-neutral-weak text-base uppercase tracking-[1.6px] font-sans-display [font-variation-settings:'wght'_800] text-background-neutral-weak">
{comparisonSections[0]?.title}
</TableHead>
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.

⚠️ Potential issue | 🟡 Minor

Don’t use the first section title as the first-column header.

Line 26 renders Managed Connection Pool in the top-left header and then repeats it again in the first section row. It also makes the header change if comparisonSections is reordered. A static label like Feature keeps the header accurate and gives the first column a real heading.

Suggested fix
-              {comparisonSections[0]?.title}
+              Feature
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableHead className="bg-background-neutral-weak text-base uppercase tracking-[1.6px] font-sans-display [font-variation-settings:'wght'_800] text-background-neutral-weak">
{comparisonSections[0]?.title}
</TableHead>
<TableHead className="bg-background-neutral-weak text-base uppercase tracking-[1.6px] font-sans-display [font-variation-settings:'wght'_800] text-background-neutral-weak">
Feature
</TableHead>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/pricing/pricing-comparison-table.tsx` around lines 25 - 27,
The TableHead currently uses comparisonSections[0]?.title which duplicates the
first row and changes when sections are reordered; replace that dynamic header
with a static label (e.g., "Feature") so the top-left cell is a fixed column
heading. Update the TableHead element in pricing-comparison-table.tsx (the
TableHead JSX that currently references comparisonSections[0]?.title) to render
the static string and leave the section rows to render their own titles from
comparisonSections.

Comment on lines +190 to +191
url: ({ current_page, text_data }: { current_page: string; text_data: string }) =>
`https://www.linkedin.com/sharing/share-offsite/?url=${current_page}`,
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where share URLs are assembled and whether encodeURIComponent is used.
rg -n -C2 'sharing/share-offsite|bsky\.app/intent/compose|x\.com/share|encodeURIComponent' packages/ui/src/data/footer.ts

Repository: prisma/web

Length of output: 770


Encode share query parameters before interpolation.

All three share URL helpers have unencoded query parameters that risk URL corruption with reserved characters:

  • Line 191 (LinkedIn): current_page needs encoding
  • Line 205–206 (X.com): text_data, current_page, and hashtags all need encoding
  • Line 213 (Bluesky): text_data and current_page need encoding

Use encodeURIComponent() to safely interpolate user-supplied values into query strings.

Proposed fix
-    url: ({ current_page, text_data }: { current_page: string; text_data: string }) =>
-      `https://www.linkedin.com/sharing/share-offsite/?url=${current_page}`,
+    url: ({ current_page, text_data }: { current_page: string; text_data: string }) =>
+      `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(current_page)}`,
@@
-      `http://x.com/share?text=${text_data}&url=${current_page}${
-        hashtags ? `&hashtags=${hashtags.join()}` : ``
-      }`,
+      `http://x.com/share?text=${encodeURIComponent(text_data)}&url=${encodeURIComponent(current_page)}${
+        hashtags ? `&hashtags=${encodeURIComponent(hashtags.join(','))}` : ``
+      }`,
@@
-    url: ({ current_page, text_data }: { current_page: string; text_data: string }) =>
-      `https://bsky.app/intent/compose?text=${text_data}${current_page}`,
+    url: ({ current_page, text_data }: { current_page: string; text_data: string }) =>
+      `https://bsky.app/intent/compose?text=${encodeURIComponent(
+        `${text_data} ${current_page}`.trim(),
+      )}`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/data/footer.ts` around lines 190 - 191, The share URL helpers
currently interpolate raw values and must encode query params: update the url
properties for the LinkedIn, X (Twitter), and Bluesky helpers so they call
encodeURIComponent(...) on all interpolated user values — for LinkedIn encode
current_page; for the X helper encode text_data, current_page, and hashtags; for
the Bluesky helper encode text_data and current_page — ensure you replace direct
${...} interpolation inside the url strings with encoded values in the
respective url functions.

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.

2 participants