fix(site): add currency conversion to pricing page#7752
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
WalkthroughPricing 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ 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. Comment |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
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>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/site/src/app/pricing/pricing-calculator.tsx (1)
254-257: Renderdescriptionas text instead of injecting HTML.
getPlanDescription()currently returns plain text, sodangerouslySetInnerHTMLbuys 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
📒 Files selected for processing (2)
apps/site/src/app/pricing/pricing-calculator.tsxapps/site/src/app/pricing/pricing-data.ts
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>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
apps/site/src/app/pricing/pricing-data.ts (1)
71-85:⚠️ Potential issue | 🟠 MajorPer-unit prices are still being whole-rounded.
Line 79 still routes any
digits <= config.decimalsvalue throughMath.round(). That breaks/GBprices for 2-decimal currencies and still leaves.00on 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"]andcolSpan={5}duplicate data that already exists inpricing-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
📒 Files selected for processing (5)
apps/site/src/app/pricing/page.tsxapps/site/src/app/pricing/pricing-comparison-table.tsxapps/site/src/app/pricing/pricing-data.tsapps/site/src/app/pricing/pricing-page-content.tsxpackages/ui/src/data/footer.ts
| <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> |
There was a problem hiding this comment.
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.
| <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.
| url: ({ current_page, text_data }: { current_page: string; text_data: string }) => | ||
| `https://www.linkedin.com/sharing/share-offsite/?url=${current_page}`, |
There was a problem hiding this comment.
🧩 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.tsRepository: 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_pageneeds encoding - Line 205–206 (X.com):
text_data,current_page, andhashtagsall need encoding - Line 213 (Bluesky):
text_dataandcurrent_pageneed 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.
Summary
convertFromUsd()helperTest plan
/pricingand switch through all 10 currencies🤖 Generated with Claude Code
Summary by CodeRabbit