From 3ed70b6999dc32f13e77f4dbe0195d4bfb5dddbf Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Fri, 14 Aug 2026 23:00:17 +0200 Subject: [PATCH] =?UTF-8?q?Aggiorna=20marketing:=20prezzi=20lancio,=20Funz?= =?UTF-8?q?ionalit=C3=A0=20narrativa=20e=20link=20store.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allinea i piani Light/Full ai prezzi di lancio, ripensa /funzionalita per vendere il prodotto e aggiunge i badge App Store/Play nella home. Co-authored-by: Cursor --- backend/app/helpers/public/billing_helper.rb | 4 + .../services/billing/payment_description.rb | 6 +- .../billing/stripe/plan_change_messages.rb | 5 +- .../services/billing/stripe/price_catalog.rb | 56 +- backend/app/views/layouts/marketing.html.erb | 2 +- .../app/views/layouts/marketing_live.html.erb | 2 +- .../app/views/public/pages/features.html.erb | 308 ++++- backend/app/views/public/pages/home.html.erb | 7 +- .../app/views/public/pages/pricing.html.erb | 49 +- .../views/shared/_marketing_footer.html.erb | 1 + backend/app/views/shared/_plan_cards.html.erb | 44 +- .../app/views/shared/_store_badges.html.erb | 31 + backend/config/initializers/match_live_tv.rb | 8 + backend/config/locales/app.de.yml | 9 +- backend/config/locales/app.en.yml | 9 +- backend/config/locales/app.es.yml | 9 +- backend/config/locales/app.fr.yml | 9 +- backend/config/locales/app.it.yml | 9 +- backend/config/locales/pages.de.yml | 117 +- backend/config/locales/pages.en.yml | 117 +- backend/config/locales/pages.es.yml | 119 +- backend/config/locales/pages.fr.yml | 119 +- backend/config/locales/pages.it.yml | 119 +- backend/config/locales/ui.de.yml | 7 + backend/config/locales/ui.en.yml | 7 + backend/config/locales/ui.es.yml | 7 + backend/config/locales/ui.fr.yml | 7 + backend/config/locales/ui.it.yml | 7 + backend/public/images/store/apple.svg | 3 + backend/public/images/store/google-play.svg | 6 + backend/public/marketing.css | 1041 ++++++++++++++++- .../helpers/public/billing_helper_spec.rb | 2 +- .../spec/requests/public/club_billing_spec.rb | 6 +- .../requests/public/features_page_spec.rb | 26 + .../spec/requests/public/home_page_spec.rb | 41 + .../spec/requests/public/pricing_page_spec.rb | 32 + .../billing/payment_description_spec.rb | 4 +- .../billing/stripe/price_catalog_spec.rb | 9 + .../billing/stripe/record_payment_spec.rb | 2 +- docs/PRODUCT_PREMIUM.md | 5 +- docs/STRIPE_PRODUCTION.md | 8 +- infra/.env.example | 2 + 42 files changed, 2115 insertions(+), 266 deletions(-) create mode 100644 backend/app/views/shared/_store_badges.html.erb create mode 100644 backend/public/images/store/apple.svg create mode 100644 backend/public/images/store/google-play.svg create mode 100644 backend/spec/requests/public/features_page_spec.rb create mode 100644 backend/spec/requests/public/home_page_spec.rb create mode 100644 backend/spec/requests/public/pricing_page_spec.rb diff --git a/backend/app/helpers/public/billing_helper.rb b/backend/app/helpers/public/billing_helper.rb index f1f9d3e..e2231cd 100644 --- a/backend/app/helpers/public/billing_helper.rb +++ b/backend/app/helpers/public/billing_helper.rb @@ -57,6 +57,10 @@ module Public Billing::Stripe::PlanChangeMessages.button_label(plan: plan, interval: interval, kind: btn_kind) end + def plan_cta_name(plan) + I18n.t("pages.plans.cta_name.#{plan.slug}", default: plan.name) + end + def billing_plan_change_info_lines(subscription: nil) Billing::Stripe::PlanChangeMessages.billing_info_lines(subscription: subscription) end diff --git a/backend/app/services/billing/payment_description.rb b/backend/app/services/billing/payment_description.rb index 289e840..9e56887 100644 --- a/backend/app/services/billing/payment_description.rb +++ b/backend/app/services/billing/payment_description.rb @@ -5,9 +5,13 @@ module Billing AMOUNT_HINTS = { 500 => %w[premium_light monthly], + 790 => %w[premium_light monthly], 4000 => %w[premium_light yearly], + 5900 => %w[premium_light yearly], 2000 => %w[premium_full monthly], - 20_000 => %w[premium_full yearly] + 2490 => %w[premium_full monthly], + 20_000 => %w[premium_full yearly], + 19_900 => %w[premium_full yearly] }.freeze class << self diff --git a/backend/app/services/billing/stripe/plan_change_messages.rb b/backend/app/services/billing/stripe/plan_change_messages.rb index ec7af36..8782cc5 100644 --- a/backend/app/services/billing/stripe/plan_change_messages.rb +++ b/backend/app/services/billing/stripe/plan_change_messages.rb @@ -23,10 +23,11 @@ module Billing def button_label(plan:, interval:, kind:) price = PriceCatalog.label(plan_slug: plan.slug, interval: interval) + name = I18n.t("pages.plans.cta_name.#{plan.slug}", default: plan.name) if kind == :checkout - I18n.t("billing.messages.activate_button", plan: plan.name, price: price) + I18n.t("billing.messages.activate_button", plan: name, price: price) else - I18n.t("billing.messages.switch_button", plan: plan.name, price: price) + I18n.t("billing.messages.switch_button", plan: name, price: price) end end diff --git a/backend/app/services/billing/stripe/price_catalog.rb b/backend/app/services/billing/stripe/price_catalog.rb index bb91ec9..79dfff8 100644 --- a/backend/app/services/billing/stripe/price_catalog.rb +++ b/backend/app/services/billing/stripe/price_catalog.rb @@ -4,9 +4,16 @@ module Billing INTERVALS = %w[monthly yearly].freeze DEFAULT_INTERVAL = "yearly" - PRICING = { - "premium_light" => { "yearly" => "€40/anno", "monthly" => "€5/mese" }, - "premium_full" => { "yearly" => "€200/anno", "monthly" => "€20/mese" } + # Importi in centesimi (EUR). Il yearly è il prezzo lancio addebitato. + AMOUNTS = { + "premium_light" => { + "yearly" => { charge: 5900, list: 7900, equivalent_monthly: 492 }, + "monthly" => { charge: 790 } + }, + "premium_full" => { + "yearly" => { charge: 19_900, list: 24_900, equivalent_monthly: 1658 }, + "monthly" => { charge: 2490 } + } }.freeze class << self @@ -30,7 +37,23 @@ module Billing end def label(plan_slug:, interval:) - PRICING.dig(plan_slug.to_s, normalize_interval(interval)) || normalize_interval(interval) + interval = normalize_interval(interval) + format_interval_price(charge_cents(plan_slug, interval), interval) + end + + def list_label(plan_slug:, interval: "yearly") + interval = normalize_interval(interval) + cents = list_cents(plan_slug, interval) + return nil if cents.blank? + + format_interval_price(cents, interval) + end + + def equivalent_monthly_label(plan_slug:) + cents = AMOUNTS.dig(plan_slug.to_s, "yearly", :equivalent_monthly) + return nil if cents.blank? + + format_interval_price(cents, "monthly") end def available_intervals(plan_slug:) @@ -52,8 +75,33 @@ module Billing nil end + def format_eur(cents) + cents = cents.to_i + whole, frac = cents.divmod(100) + if frac.zero? + "€#{whole}" + else + sep = I18n.locale.to_s == "en" ? "." : "," + "€#{whole}#{sep}#{frac.to_s.rjust(2, '0')}" + end + end + private + def charge_cents(plan_slug, interval) + AMOUNTS.dig(plan_slug.to_s, interval, :charge) + end + + def list_cents(plan_slug, interval) + AMOUNTS.dig(plan_slug.to_s, interval, :list) + end + + def format_interval_price(cents, interval) + return nil if cents.blank? + + I18n.t("billing.prices.#{interval}", amount: format_eur(cents)) + end + def price_id_for(plan_slug, interval) case [plan_slug, interval] when %w[premium_light monthly] diff --git a/backend/app/views/layouts/marketing.html.erb b/backend/app/views/layouts/marketing.html.erb index 11303a1..c1c353a 100644 --- a/backend/app/views/layouts/marketing.html.erb +++ b/backend/app/views/layouts/marketing.html.erb @@ -8,7 +8,7 @@ <%= render "shared/meta_tags" %> <%= yield :head %> - + data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>> <%= render "shared/cookie_banner" %> diff --git a/backend/app/views/layouts/marketing_live.html.erb b/backend/app/views/layouts/marketing_live.html.erb index 048d895..7294dac 100644 --- a/backend/app/views/layouts/marketing_live.html.erb +++ b/backend/app/views/layouts/marketing_live.html.erb @@ -6,7 +6,7 @@ <%= content_for?(:title) ? yield(:title) : "Match Live TV" %> <%= render "shared/meta_tags" %> - + <%= yield :head %> diff --git a/backend/app/views/public/pages/features.html.erb b/backend/app/views/public/pages/features.html.erb index 2ee67a4..ba4ea40 100644 --- a/backend/app/views/public/pages/features.html.erb +++ b/backend/app/views/public/pages/features.html.erb @@ -2,63 +2,283 @@ <% content_for :meta_description, t("pages.features.meta_description") %> <% content_for :canonical_url, seo_absolute_url(public_features_path) %> -
-

<%= t("pages.features.title") %>

-

<%= t("pages.features.lead") %>

+
+
+
+
+

Match Live TV

+

<%= t("pages.features.hero_title") %>

+

<%= t("pages.features.hero_lead") %>

-
-

<%= t("pages.features.staff_section_title") %>

-
-
-

<%= t("pages.features.staff_invite_title") %>

-

<%= t("pages.features.staff_invite_body") %>

+
    "> +
  1. + + <%= t("pages.features.flow_phone") %> +
  2. + +
  3. + + <%= t("pages.features.flow_account") %> +
  4. + +
  5. + + <%= t("pages.features.flow_live") %> +
  6. +
-
-

<%= t("pages.features.staff_access_title") %>

-

<%= t("pages.features.staff_access_body") %>

+
+ <%= image_tag "/hero-devices.png", + alt: t("pages.features.hero_alt"), + class: "features-hero__img", + loading: "eager", + fetchpriority: "high" %>
-
-

<%= t("pages.features.staff_plans_title") %>

-

<%= t("pages.features.staff_plans_body") %>

+
+
+ +
+

<%= t("pages.features.how_title") %>

+
    +
  1. + +

    <%= t("pages.features.how_1_title") %>

    +

    <%= t("pages.features.how_1_body") %>

    +
  2. +
  3. + +

    <%= t("pages.features.how_2_title") %>

    +

    <%= t("pages.features.how_2_body") %>

    +
  4. +
  5. + +

    <%= t("pages.features.how_3_title") %>

    +

    <%= t("pages.features.how_3_body") %>

    +
  6. +
  7. + +

    <%= t("pages.features.how_4_title") %>

    +

    <%= t("pages.features.how_4_body") %>

    +
  8. +
  9. + +

    <%= t("pages.features.how_5_title") %>

    +

    <%= t("pages.features.how_5_body") %>

    +
  10. +
+
+ +
+
+

<%= t("pages.features.accounts_title") %>

+

<%= t("pages.features.accounts_lead") %>

+
+
+ +

<%= t("pages.features.accounts_invite_title") %>

+

<%= t("pages.features.accounts_invite_body") %>

+
+
+ +

<%= t("pages.features.accounts_transmit_title") %>

+

<%= t("pages.features.accounts_transmit_body") %>

+
+
+ +

<%= t("pages.features.accounts_revoke_title") %>

+

<%= t("pages.features.accounts_revoke_body") %>

+
+
+

<%= t("pages.features.accounts_highlight") %>

+
+
+ +
+
+
+

<%= t("pages.features.score_title") %>

+

<%= t("pages.features.score_lead") %>

+
    +
  • <%= t("pages.features.score_item_app") %>
  • +
  • <%= t("pages.features.score_item_signup") %>
  • +
  • <%= t("pages.features.score_item_password") %>
  • +
  • <%= t("pages.features.score_item_link") %>
  • +
+
+ +
+
+ +
+

<%= t("pages.features.distribute_title") %>

+

<%= t("pages.features.distribute_lead") %>

+
    "> +
  1. + + <%= t("pages.features.distribute_phone") %> +
  2. + +
  3. + + <%= t("pages.features.distribute_mltv") %> +
  4. + +
  5. +
    + + <%= t("pages.features.distribute_viewers") %> +
    +
    + + <%= t("pages.features.distribute_youtube") %> +
    +
  6. +
+
+ +
+
+
+ <%= t("pages.features.youtube_badge") %> +

<%= t("pages.features.youtube_title") %>

+

<%= t("pages.features.youtube_body") %>

+
+
-
-

<%= t("pages.features.live_section_title") %>

-
+
+
+
+

<%= t("pages.features.replay_title") %>

+

<%= t("pages.features.replay_lead") %>

+
    +
  • <%= t("pages.features.replay_item_server") %>
  • +
  • <%= t("pages.features.replay_item_plan") %>
  • +
  • <%= t("pages.features.replay_item_download") %>
  • +
+
+ +
+
+ +
+

<%= t("pages.features.more_title") %>

+
-

<%= t("pages.features.live_phone_title") %>

-

<%= t("pages.features.live_phone_body") %>

+ +

<%= t("pages.features.more_stable_title") %>

+

<%= t("pages.features.more_stable_body") %>

-

<%= t("pages.features.live_score_title") %>

-

<%= t("pages.features.live_score_body") %>

+ +

<%= t("pages.features.more_link_title") %>

+

<%= t("pages.features.more_link_body") %>

-

<%= t("pages.features.live_link_title") %>

-

<%= t("pages.features.live_link_body") %>

-
-
-

<%= t("pages.features.live_archive_title") %>

-

<%= t("pages.features.live_archive_body") %>

-
-
-

<%= t("pages.features.live_youtube_mltv_title") %>

-

<%= t("pages.features.live_youtube_mltv_body") %>

-
-
-

<%= t("pages.features.live_youtube_club_title") %>

-

<%= t("pages.features.live_youtube_club_body") %>

-
-
-

<%= t("pages.features.live_website_title") %>

-

<%= t("pages.features.live_website_body") %>

+ +

<%= t("pages.features.more_yt_mltv_title") %>

+

<%= t("pages.features.more_yt_mltv_body") %>

-

- <%= link_to t("pages.features.cta_signup"), public_signup_path, class: "btn btn-primary" %> -

-
+
+
+

<%= t("pages.features.cta_title") %>

+

<%= t("pages.features.cta_body") %>

+
+ <%= link_to t("pages.features.cta_primary"), public_signup_path, class: "btn btn-primary" %> + <%= link_to t("pages.features.cta_secondary"), public_prezzi_path, class: "btn btn-outline" %> +
+
+
+
diff --git a/backend/app/views/public/pages/home.html.erb b/backend/app/views/public/pages/home.html.erb index 920ca3d..1d96b68 100644 --- a/backend/app/views/public/pages/home.html.erb +++ b/backend/app/views/public/pages/home.html.erb @@ -10,7 +10,8 @@ "url" => seo_absolute_url(root_path), "applicationCategory" => "SportsApplication", "operatingSystem" => "Web, Android, iOS", - "description" => t("home.schema_description") + "description" => t("home.schema_description"), + "sameAs" => [MatchLiveTv.app_store_url, MatchLiveTv.play_store_url] }.to_json) %> <% end %> @@ -26,6 +27,10 @@ <%= link_to t("home.cta_signup"), public_signup_path, class: "btn btn-primary" %> <%= link_to t("home.cta_live"), public_live_index_path, class: "btn btn-secondary" %>
+
+

<%= t("home.stores_label") %>

+ <%= render "shared/store_badges", variant: "hero" %> +