diff --git a/.gitignore b/.gitignore index 97646259f..e34e4a8df 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ seo-report.sarif # Generated local D1 fixture for builder-data screenshots apps/web/test/fixtures/builder-metrics-d1-seed.sql + +# Local Playwright MCP session logs +.playwright-mcp/ diff --git a/_typos.toml b/_typos.toml index f5e7be6ce..56c0682ae 100644 --- a/_typos.toml +++ b/_typos.toml @@ -32,6 +32,8 @@ extend-ignore-re = [ "https?://\\S+", # Cloudflare data center (IATA airport) codes in the hero world map config "colo: '[A-Z]{3}'", + # ISO 3166 country code for the Faroe Islands in the pricing currency map + "FO: 'DKK'", # IndexNow verification key "ue6dw2uu67eum6y2m8en775jjf1qhe1r", ] diff --git a/apps/shared/copy/messageContexts.ts b/apps/shared/copy/messageContexts.ts index 08a88fd6a..0e7fbfd18 100644 --- a/apps/shared/copy/messageContexts.ts +++ b/apps/shared/copy/messageContexts.ts @@ -3366,6 +3366,11 @@ export const messageContexts = { pricing_credits_cover_usage: 'Page/area: Pricing page or plan calculator UI. Role: Website copy sentence. Seen in: component pricing/CreditPricing.astro. Message key `pricing_credits_cover_usage` (Pricing Credits Cover Usage).', pricing_credits_cta_desktop: 'Page/area: Pricing page or plan calculator UI. Role: Website copy sentence. Message key `pricing_credits_cta_desktop` (Pricing Credits CTA Desktop).', pricing_credits_cta_mobile: 'Page/area: Pricing page or plan calculator UI. Role: Website copy sentence. Message key `pricing_credits_cta_mobile` (Pricing Credits CTA Mobile).', + pricing_currency_auto: 'Page/area: Pricing page or plan calculator UI. Role: Short UI label or navigation item. Seen in: component pricing/CurrencySelector.astro. Message key `pricing_currency_auto` (Pricing Currency Auto).', + pricing_currency_change: 'Page/area: Pricing page or plan calculator UI. Role: Short UI label or navigation item. Seen in: component pricing/Plans.astro. Message key `pricing_currency_change` (Pricing Currency Change).', + pricing_currency_estimate_note: 'Page/area: Pricing page or plan calculator UI. Role: Supporting note or footnote. Seen in: component pricing/Plans.astro. Message key `pricing_currency_estimate_note` (Pricing Currency Estimate Note).', + pricing_currency_hint: 'Page/area: Pricing page or plan calculator UI. Role: Website copy sentence. Seen in: component pricing/CurrencySelector.astro. Preserve Capgo product/brand and developer terms exactly. Message key `pricing_currency_hint` (Pricing Currency Hint).', + pricing_currency_label: 'Page/area: Pricing page or plan calculator UI. Role: UI label. Seen in: component pricing/CurrencySelector.astro. Message key `pricing_currency_label` (Pricing Currency Label).', pricing_description: 'Page/area: Pricing page or plan calculator UI. Role: Supporting description paragraph or meta description. Seen in: page pricing.astro. Preserve Capgo product/brand and developer terms exactly. Message key `pricing_description` (Pricing Description).', pricing_explainer_bandwidth_body: 'Page/area: Pricing page or plan calculator UI. Role: Long marketing or legal paragraph. Seen in: component pricing/PricingExplainer.astro. Message key `pricing_explainer_bandwidth_body` (Pricing Explainer Bandwidth Body).', pricing_explainer_bandwidth_title: 'Page/area: Pricing page or plan calculator UI. Role: Section or page heading. Seen in: component pricing/PricingExplainer.astro. Message key `pricing_explainer_bandwidth_title` (Pricing Explainer Bandwidth Title).', diff --git a/apps/shared/copy/messages.ts b/apps/shared/copy/messages.ts index 442f94095..25d559757 100644 --- a/apps/shared/copy/messages.ts +++ b/apps/shared/copy/messages.ts @@ -1376,6 +1376,11 @@ const messages = { 'EAS prices from expo.dev/pricing, checked September 2026: cheapest plan plus $0.005 per extra MAU. Compares MAU cost only; bandwidth, storage, and native builds are not included. Capgo prices are per month, billed yearly.', pricing_compare_plan_suffix: 'plan', pricing_compare_title: 'What live updates cost at your scale', + pricing_currency_auto: 'Based on your location', + pricing_currency_change: 'Change currency', + pricing_currency_estimate_note: 'Prices converted from USD at daily exchange rates. You are billed in USD.', + pricing_currency_hint: 'Capgo bills in USD. Other currencies are estimates based on daily European Central Bank exchange rates.', + pricing_currency_label: 'Display prices in', pricing_credits_cta_desktop: "Don't want a plan? Buy credits instead.", pricing_credits_cta_mobile: 'Buy credits instead.', pricing_calculator_cta: 'Calculate your price', diff --git a/apps/shared/copy/translationContextByText.ts b/apps/shared/copy/translationContextByText.ts index 1d0498576..194235de2 100644 --- a/apps/shared/copy/translationContextByText.ts +++ b/apps/shared/copy/translationContextByText.ts @@ -719,6 +719,7 @@ export const translationContextByText: Record = { 'Banking app with account, payment, loan, and offer flows that need controlled rollout.': 'Page/area: Solution page app examples section. Role: Supporting note or footnote. Message key `solution_app_examples_fintech_app_1_note` (Solution App Examples Fintech App 1 Note).', 'Banking apps need safe, staged interface fixes': 'Page/area: Solution page app examples section. Role: Section or page heading. Message key `solution_app_examples_fintech_headline` (Solution App Examples Fintech Headline).', 'Based on public competitor cloud docs and pricing, plus GitHub Actions macOS rates.': 'Page/area: Capgo Builder / native cloud build product page. Role: Supporting note or footnote. Seen in: page native-build.astro. Preserve Capgo product/brand and developer terms exactly. Message key `native_build_v2_comp_footnote` (Native Build V2 Comp Footnote).', + 'Based on your location': 'Page/area: Pricing page or plan calculator UI. Role: Short UI label or navigation item. Seen in: component pricing/CurrencySelector.astro. Message key `pricing_currency_auto` (Pricing Currency Auto).', 'Basic automatic updates are well documented. But Capgo has many options (manual, half-manual, channel_default, etc.) that aren\'t always easy to understand for every use case yet. We\'re actively improving this.': 'Page/area: Capacitor live-update alternatives comparison page. Role: Long marketing or legal paragraph. Seen in: page alternatives.astro. Preserve Capgo product/brand and developer terms exactly. Message key `alternatives_not_good_at_docs` (Alternatives Not Good At Docs).', 'Basic bundle checks.': 'Page/area: Live updates product page. Role: Website copy sentence. Seen in: page live-update.astro. Preserve Capgo product/brand and developer terms exactly. Message key `live_update_v2_comp_validate_capa` (Live Update V2 Comp Validate Capa).', 'Basic bundle validation. Native drift is easy to miss.': 'Page/area: CLI product or documentation marketing copy. Role: Website copy sentence. Seen in: page capgo-cli.astro. Preserve Capgo product/brand and developer terms exactly. Message key `cli_v2_comp_compat_capa` (CLI V2 Comp Compat Capa).', @@ -1110,6 +1111,7 @@ export const translationContextByText: Record = { 'Capgo Backend & Landing': 'Page/area: Bug bounty program page. Role: Short UI label or navigation item. Seen in: page bug-bounty.astro. Preserve Capgo product/brand and developer terms exactly. Message key `bug_bounty_repo_landing` (Bug Bounty Repo Landing).', 'Capgo backend and product repository (capgo.app API, dashboard, and related services)': 'Page/area: Bug bounty program page. Role: Website copy sentence. Seen in: page bug-bounty.astro. Preserve Capgo product/brand and developer terms exactly. Message key `bug_bounty_repo_landing_desc` (Bug Bounty Repo Landing Desc).', 'Capgo beta channel': 'Page/area: Capgo solutions marketing page. Role: Short UI label or navigation item. Seen in: page solutions/beta-testing.astro. Preserve Capgo product/brand and developer terms exactly. Message key `solutions_beta_testing_compare_capgo` (Solutions Beta Testing Compare Capgo).', + 'Capgo bills in USD. Other currencies are estimates based on daily European Central Bank exchange rates.': 'Page/area: Pricing page or plan calculator UI. Role: Website copy sentence. Seen in: component pricing/CurrencySelector.astro. Preserve Capgo product/brand and developer terms exactly. Message key `pricing_currency_hint` (Pricing Currency Hint).', 'Capgo brand resources': 'Page/area: Capgo marketing website. Role: Section or page heading. Seen in: page brand.astro. Preserve Capgo product/brand and developer terms exactly. Message key `brand_hero_title` (Brand Hero Title).', 'Capgo Build': 'Page/area: Capgo solutions marketing page. Role: Short UI label or navigation item. Seen in: page solutions/white-label.astro. Preserve Capgo product/brand and developer terms exactly. Message key `solutions_white_label_visual_cell3_value` (Solutions White Label Visual Cell3 Value).', 'Capgo Build compiles and signs': 'Page/area: Capgo solutions marketing page. Role: Section or page heading. Preserve Capgo product/brand and developer terms exactly. Message key `solutions_webapp_to_mobile_cap2_title` (Solutions Webapp To Mobile Cap2 Title).', @@ -1317,6 +1319,7 @@ export const translationContextByText: Record = { 'Certificates and profiles are used in memory during the build and deleted when it finishes. Each build runs in an isolated environment that is destroyed afterwards.': 'Page/area: Capgo solutions marketing page. Role: Long marketing or legal paragraph. Message key `solutions_build_without_mac_trust3_desc` (Solutions Build Without Mac Trust3 Desc).', 'Certificates and profiles often sit on one person\'s Mac. When that person is away or a profile expires, the release waits.': 'Page/area: Capgo solutions marketing page. Role: Website copy sentence. Message key `solutions_build_without_mac_pain2_desc` (Solutions Build Without Mac Pain2 Desc).', 'Certificates, provisioning profiles, and keystores are supplied at build time from local env vars or CI secrets.': 'Page/area: Capgo Builder / native cloud build product page. Role: Website copy sentence. Message key `native_build_builder_feature_signing_text` (Native Build Builder Feature Signing Text).', + 'Change currency': 'Page/area: Pricing page or plan calculator UI. Role: Short UI label or navigation item. Seen in: component pricing/Plans.astro. Message key `pricing_currency_change` (Pricing Currency Change).', 'Change language': 'Page/area: Site footer navigation or footer blurb. Role: UI label. Seen in: site footer. Message key `footer_language_label` (Footer Language Label).', 'Change promo and checkout screens during a campaign': 'Page/area: Capgo solutions marketing page. Role: Section or page heading. Seen in: page solutions/ecommerce.astro. Message key `solutions_ecommerce_hero_title` (Solutions Ecommerce Hero Title).', 'Change promo, product, and checkout screens in your Capacitor shopping app during a campaign. Stage on a channel, roll out by percentage, and roll back without a store release.': 'Page/area: Capgo solutions marketing page. Role: Supporting description paragraph or meta description. Seen in: page solutions/ecommerce.astro. Preserve Capgo product/brand and developer terms exactly. Message key `solutions_ecommerce_description` (Solutions Ecommerce Description).', @@ -1812,6 +1815,7 @@ export const translationContextByText: Record = { 'Discord community, support@capgo.app, and the contact page': 'Page/area: About Capgo page. Role: Website copy sentence. Seen in: page about.astro. Preserve Capgo product/brand and developer terms exactly. Message key `about_facts_communication_value` (About Facts Communication Value).', 'Discord support': 'Page/area: Enterprise product/pricing page. Role: Short UI label or navigation item. Seen in: page enterprise.astro. Preserve Capgo product/brand and developer terms exactly. Message key `enterprise_discord_support` (Enterprise Discord Support).', 'Discover how Capgo safeguards over-the-air updates with layered security, transparent monitoring, and privacy-first operations.': 'Page/area: Trust / security / compliance page. Role: SEO meta description. Seen in: page trust.astro. Preserve Capgo product/brand and developer terms exactly. Message key `trust_meta_description` (Trust Meta Description).', + 'Display prices in': 'Page/area: Pricing page or plan calculator UI. Role: UI label. Seen in: component pricing/CurrencySelector.astro. Message key `pricing_currency_label` (Pricing Currency Label).', 'Disputes Resolution': 'Page/area: Terms of Service legal page. Role: Section or page heading. Seen in: page tos.astro. Message key `tos_disputes_resolution_title` (Terms of Service Disputes Resolution Title).', 'dist': 'HTML text fragment from a longer Capgo UI string (parent key `solutions_cordova_to_capacitor_ai_step2_body`). Page/area: Capgo solutions marketing page. Role: Website copy sentence. Seen in: page solutions/cordova-to-capacitor-ai.astro. Message key `solutions_cordova_to_capacitor_ai_step2_body` (Solutions Cordova To Capacitor Ai Step2 Body).', 'Distribution certificate': 'Page/area: Capgo Builder / native cloud build product page. Role: Short UI label or navigation item. Seen in: page native-build.astro. Message key `native_build_v2_wiz_ios_s2_l` (Native Build V2 Wiz Ios S2 L).', @@ -3861,6 +3865,7 @@ export const translationContextByText: Record = { 'Preview each lockup, then download SVG or PNG. Files are production ready and match the vectors used on capgo.app.': 'Page/area: Capgo marketing website. Role: Website copy sentence. Seen in: page brand.astro. Message key `brand_logos_intro` (Brand Logos Intro).', 'Preview every pull request on device': 'Page/area: Solution page app examples section. Role: Bullet list item. Message key `solution_app_examples_pr_preview_play_1` (Solution App Examples Pr Preview Play 1).', 'Preview your next pull request on a phone': 'Page/area: Capgo solutions marketing page. Role: Section or page heading. Seen in: page solutions/pr-preview.astro. Message key `solutions_pr_final_cta_title` (Solutions Pr Final CTA Title).', + 'Prices converted from USD at daily exchange rates. You are billed in USD.': 'Page/area: Pricing page or plan calculator UI. Role: Supporting note or footnote. Seen in: component pricing/Plans.astro. Message key `pricing_currency_estimate_note` (Pricing Currency Estimate Note).', 'Prices usually come from your backend or POS integration, so change them there. A live update changes the web screens that display the menu and prices. Native plugins, payment SDKs, and Capacitor upgrades need a store release.': 'Page/area: Capgo solutions marketing page. Role: Long marketing or legal paragraph. Seen in: page solutions/qsr.astro. Preserve Capgo product/brand and developer terms exactly. Message key `solutions_qsr_faq5_a` (Solutions Qsr Faq5 A).', 'Prices usually come from your backend, so change them there. A live update changes the web code and assets that display them: promo banners, product pages, cart and checkout screens. Changes to native payment SDKs or plugins need a store release.': 'Page/area: Capgo solutions marketing page. Role: Long marketing or legal paragraph. Seen in: page solutions/ecommerce.astro. Preserve Capgo product/brand and developer terms exactly. Message key `solutions_ecommerce_faq1_a` (Solutions Ecommerce Faq1 A).', 'Pricing': 'Navigation/label for the Pricing page. Page/area: About Capgo page. Role: Short UI label or navigation item. Seen in: page about.astro. Message key `about_facts_pricing` (About Facts Pricing). | Navigation/label for the Pricing page. Page/area: Capgo Builder / native cloud build product page. Role: Section subtitle or tagline. Seen in: page native-build.astro. Message key `native_build_builder_pricing_eyebrow` (Native Build Builder Pricing Eyebrow). | Navigation/label for the Pricing page. Page/area: Capgo marketing website. Role: Call-to-action button or link label. Seen in: component BlogMidArticleCta.astro. Message key `blog_mid_article_china_cta_pricing_button` (Blog Mid Article China CTA Pricing Button). | Navigation/label for the Pricing page. Page/area: Capgo marketing website. Role: Short UI label or navigation item. Seen in: component comparison/ComparisonPage.astro. Message key `compare_row_pricing` (Compare Row Pricing). | Navigation/label for the Pricing page. Page/area: Capgo marketing website. Role: Short UI label or navigation item. Seen in: site footer, site header, page agents/[slug].astro. Message key `pricing` (Pricing).', diff --git a/apps/web/astro.config.mjs b/apps/web/astro.config.mjs index 60612c3c3..4d1d5472a 100644 --- a/apps/web/astro.config.mjs +++ b/apps/web/astro.config.mjs @@ -44,6 +44,7 @@ export default defineConfig({ proxy: { '/live-update-metrics.json': { target: 'https://capgo.app', changeOrigin: true }, '/builder-metrics.json': { target: 'https://capgo.app', changeOrigin: true }, + '/currency.json': { target: 'https://capgo.app', changeOrigin: true }, }, }, }, diff --git a/apps/web/src/components/pricing/ComparePlans.astro b/apps/web/src/components/pricing/ComparePlans.astro index 1ffa226a4..634924ba8 100644 --- a/apps/web/src/components/pricing/ComparePlans.astro +++ b/apps/web/src/components/pricing/ComparePlans.astro @@ -40,10 +40,12 @@ function buildTimeDisplay(buildTimeSeconds: number) { return `${buildTimeMinutes} ${m.build_minutes({}, { locale: Astro.locals.locale })}` } -function priceDisplay(plan: Database['public']['Tables']['plans']['Row']) { - const yearlyMonthly = (plan.price_y / 12).toFixed(0) - const suffix = plan.name === 'Enterprise' ? '+' : '' - return `$${yearlyMonthly}${suffix}/mo yearly or $${plan.price_m}${suffix}/mo monthly` +function priceParts(plan: Database['public']['Tables']['plans']['Row']) { + return { + yearlyMonthly: Math.round(plan.price_y / 12), + monthly: plan.price_m, + suffix: plan.name === 'Enterprise' ? '+' : '', + } } function included(planName: string, names: string[]) { @@ -369,7 +371,12 @@ const rows: CompareSection[] = [ {m.most_popular({}, { locale: Astro.locals.locale })} )} -

{priceDisplay(plan)}

+

+ {`$${priceParts(plan).yearlyMonthly}${priceParts(plan).suffix}`}/mo + yearly or + {`$${priceParts(plan).monthly}${priceParts(plan).suffix}`}/mo + monthly +

))} diff --git a/apps/web/src/components/pricing/CreditPricing.astro b/apps/web/src/components/pricing/CreditPricing.astro index d5fbcceb1..98396d30d 100644 --- a/apps/web/src/components/pricing/CreditPricing.astro +++ b/apps/web/src/components/pricing/CreditPricing.astro @@ -128,7 +128,7 @@ function formatPrice(price: number): string { : `${m.next({}, { locale: Astro.locals.locale })} ${formatNumber(credit.step_max - credit.step_min, type, credit.unit_factor)}`}
- ${formatPrice(credit.price_per_unit)} {getUnitLabel(type)} + ${formatPrice(credit.price_per_unit)} {getUnitLabel(type)}
))} diff --git a/apps/web/src/components/pricing/CurrencySelector.astro b/apps/web/src/components/pricing/CurrencySelector.astro new file mode 100644 index 000000000..783e870c0 --- /dev/null +++ b/apps/web/src/components/pricing/CurrencySelector.astro @@ -0,0 +1,55 @@ +--- +import m from '@/copy/messages' +import { currencyName, SUPPORTED_CURRENCIES } from '@/lib/currency' + +const locale = Astro.locals.locale +--- + +
+
+ + +
+

{m.pricing_currency_hint({}, { locale })}

+
+ + diff --git a/apps/web/src/components/pricing/Plans.astro b/apps/web/src/components/pricing/Plans.astro index 63a7047c0..7c2bae17e 100644 --- a/apps/web/src/components/pricing/Plans.astro +++ b/apps/web/src/components/pricing/Plans.astro @@ -47,12 +47,12 @@ function planHighlights(plan: Database['public']['Tables']['plans']['Row']) { return highlights } -function priceDisplay(monthlyPrice: number, yearlyPrice: number, name: string) { - if (yearly) { - const price = (yearlyPrice / 12).toFixed(0) - return name === 'Enterprise' && !enterpriseScale ? `$${price}+` : `$${price}` - } - return name === 'Enterprise' && !enterpriseScale ? `$${monthlyPrice}+` : `$${monthlyPrice}` +function priceAmount(monthlyPrice: number, yearlyPrice: number) { + return yearly ? Math.round(yearlyPrice / 12) : monthlyPrice +} + +function priceSuffix(name: string) { + return name === 'Enterprise' && !enterpriseScale ? '+' : '' } function buildTimeDisplay(buildTimeSeconds: number) { @@ -95,13 +95,19 @@ function buildTimeDisplay(buildTimeSeconds: number) {
-

- {priceDisplay(plan.price_m, plan.price_y, plan.name)} +

+ ${priceAmount(plan.price_m, plan.price_y)}{priceSuffix(plan.name)}

/{m.month({}, { locale: Astro.locals.locale })}

- {m.billed_annually_at({}, { locale: Astro.locals.locale })} ${numberWithSpaces(plan.price_y)} + {m.billed_annually_at({}, { locale: Astro.locals.locale })} ${numberWithSpaces(plan.price_y)}

{m.billed_monthly({}, { locale: Astro.locals.locale })} @@ -239,6 +245,10 @@ function buildTimeDisplay(buildTimeSeconds: number) {

))} + {enterpriseScale && enterprisePlan && ( @@ -273,7 +283,8 @@ function buildTimeDisplay(buildTimeSeconds: number) { const monthlyPriceElement = planElement.querySelector('[data-monthly-price]') if (priceElement) { - const price = isYearly ? (plan.price_y / 12).toFixed() : plan.price_m + const price = isYearly ? Math.round(plan.price_y / 12) : plan.price_m + priceElement.setAttribute('data-price-amount', String(price)) priceElement.textContent = planName === 'Enterprise' ? `$${price}+` : `$${price}` } @@ -285,6 +296,8 @@ function buildTimeDisplay(buildTimeSeconds: number) { monthlyPriceElement.style.display = isYearly ? 'none' : '' } }) + // Re-applies the visitor's display currency, see src/scripts/currency.ts. + document.dispatchEvent(new Event('capgo:prices-updated')) } // Handle FAQ info icon clicks @@ -335,7 +348,8 @@ function buildTimeDisplay(buildTimeSeconds: number) {