Skip to content

Charge Pro in EUR or USD #9

Description

@ezzcodeezzlife

Summary

Let customers pay Pro in EUR or USD, instead of USD-only. The product is already used from Europe (operator timezone, .dev SaaS); showing $20/mo only adds FX surprise on EU cards and looks US-centric.

This is presentment currency, not a different plan. Same Free/Pro limits (10 vs 100 / 30 days).

Current behavior

  • Stripe product/price bootstrap: scripts/setup-stripe-plans.mjs creates one recurring price, currency: "usd", unit_amount: 2000, lookup key feedback2code_pro_monthly_20_usd.
  • Checkout (app/api/billing/checkout/route.ts) always uses getProPriceId()STRIPE_PRO_PRICE_ID. No currency, no second price, no Adaptive Pricing flags.
  • Landing (components/home/landing-view.tsx) and Account (components/account/billing-actions.tsx) hardcode $20 / $20/mo.
  • JSON-LD offer is priceCurrency: "USD" (components/seo/home-next-seo.tsx).
  • User.stripePriceId is stored (lib/stripe-subscription-user.ts) — we can already tell which price they subscribed to, but the app never branches on it for display.
  • Webhook maps any active subscription to planTier: PRO regardless of currency. Good — keep that.

There is no VAT/tax UI. Stripe Tax is not wired (checkout.sessions.create has no automatic_tax).

Problem

EU agency cards get USD conversion + issuer fees. Some buyers bounce when the only number is $20. Conversely, US buyers should keep USD.

We should not silently charge EUR using Stripe’s conversion of the USD price at checkout unless we have decided that is the product (Adaptive Pricing). A named EUR sticker price is clearer for agencies (“€19/mo” or “€20/mo”).

Proposed design

Price

Create a second Stripe recurring price on the same Pro product, e.g.:

  • USD: existing 2000 / month (keep lookup key)
  • EUR: pick one and freeze it:
    • €19 — slightly cheaper sticker, common SaaS psychology, or
    • €20 — parity, easier support (“twenty a month”)

Assumption for this issue: €20 / month (parity with USD). Document that FX will not be 1:1 in their bank statement and we will not monthly-adjust EUR to track USD.

Env:

STRIPE_PRO_PRICE_ID          # USD (existing)
STRIPE_PRO_PRICE_ID_EUR      # new

Extend scripts/setup-stripe-plans.mjs to ensureRecurringPrice(..., "eur", 2000) with lookup feedback2code_pro_monthly_20_eur.

How the user chooses

v1: explicit toggle, not geo-IP.

  • Landing pricing: USD | EUR segmented control; default from Intl / navigator.language (de/fr/nl/… → EUR, else USD) but always overridable.
  • Account upgrade button uses the same preference.
  • Persist User.preferredCurrency (usd | eur) when they toggle or at checkout.

Do not infer from IP (VPNs, US laptop in Berlin, agencies billing a US parent company). Language + manual toggle is enough.

Checkout: pass the matching price id. Do not put both line items on one session.

Existing subscribers

  • Already on USD: stay on USD. Portal (app/api/billing/portal/route.ts) can change payment method; changing currency mid-subscription is a Stripe headache (cancel + resubscribe or subscription update with proration). v1: no in-app currency switch for active Pro. Show “you’re billed in USD; contact us to switch” or a later portal flow.
  • Display on Account: format using stripePriceId (if it matches EUR price → €20, else $20). Stop hardcoding $20 for Pro users who paid EUR.

Landing / SEO

  • Default render still needs a price in HTML for crawlers: keep $20 as default in SSR, hydrate the toggle. Put both amounts in visible copy: “$20 or €20 / month”.
  • JSON-LD: can emit two Offers or omit precise price (Free offer is already price: 0 USD). Follow-up: AggregateOffer low/high. Do not claim EUR-only in SoftwareApplication if the default crawl is USD.

Tax (call out, do not bury)

Charging EUR without thinking about VAT is the real EU foot-gun. This issue’s v1 can ship currency without Stripe Tax, but:

  • If you are VAT-registered in the EU, turn on Stripe Tax / automatic_tax: { enabled: true } and collect billing country in Checkout (billing_address_collection: "required" or automatic).
  • If you are not: still collecting EUR does not magically create a VAT obligation, but you should confirm with an accountant.

Implementation assumption: add billing_address_collection: "auto" (or required) when introducing EUR; leave automatic_tax as a checklist item in this issue, implement if keys/tax origin are already in Stripe Dashboard.

Assumptions

  • One Pro SKU, two presentment currencies. No “EU plan” with different quota.
  • No GBP/CHF in v1.
  • Promo codes (allow_promotion_codes: true already on checkout) must exist for both prices or be product-level.
  • Free remains 0 in both; no checkout.
  • Enterprise stays “contact us” (landing-view.tsx third card).

Acceptance criteria

  • Stripe EUR monthly price exists; STRIPE_PRO_PRICE_ID_EUR documented in .env.example.
  • Landing + Account can select USD/EUR; checkout uses the matching price id.
  • Preference stored and reused on the next checkout attempt.
  • Active USD subscribers are not migrated automatically.
  • Account shows the currency they actually pay (from stripePriceId), not always $20.
  • Webhook still sets PRO for either price.
  • Quota / welcome / PR emails that mention money (quota CTA) use a shared formatter, not a leftover $20 string.

Out of scope

  • Annual billing.
  • Purchasing power parity beyond EUR/USD.
  • Invoicing / SEPA-only methods beyond what Stripe Checkout already enables for that currency (payment_method_types: ["card"] today — consider letting Checkout auto payment methods per currency as a sub-task: drop the hard-coded card so SEPA appears for EUR).

Implementation notes

Primary files:

  • scripts/setup-stripe-plans.mjs
  • lib/billing.tsgetProPriceId(currency)
  • app/api/billing/checkout/route.ts — read currency from body + user preference
  • components/home/landing-view.tsx, components/account/billing-actions.tsx
  • prisma/schema.prismapreferredCurrency
  • .env.example

Checkout payment_method_types: ["card"] should be revisited when adding EUR (SEPA Debit is normal for EU B2B). Prefer omitting payment_method_types so Stripe Dashboard settings apply.

See also

  • Quota almost-full/full emails (upgrade CTA amount)
  • Use-case landings (EUR default more relevant on /for/agencies if we geo/language hint)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:billingStripe, plans, quota, currencyenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions