Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc2257efd2 | ||
|
|
4af4fa68ac | ||
|
|
45bdda7c95 | ||
|
|
b926531447 | ||
|
|
da6fc1d523 | ||
|
|
1d1cbf9f3f | ||
|
|
dd9901519a | ||
|
|
53449c5d3c | ||
|
|
db908e3109 | ||
|
|
5857f60d79 |
@@ -9,13 +9,23 @@ module Admin
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if params[:password].blank? || params[:password].length < 8
|
||||
flash.now[:alert] = t("admin.flash.password_too_short")
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("admin.flash.password_mismatch")
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("admin.flash.password_mismatch")
|
||||
if (code = PasswordComplexity.violation(params[:password]))
|
||||
key = case code
|
||||
when :blank, :too_short then :password_too_short
|
||||
when :too_long then :password_too_long
|
||||
else :password_too_weak
|
||||
end
|
||||
flash.now[:alert] = t("admin.flash.#{key}")
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if PasswordComplexity.same_as_current?(current_admin_account, params[:password])
|
||||
flash.now[:alert] = t("admin.flash.password_same_as_current")
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ module Api
|
||||
def update
|
||||
name = params[:name].to_s.strip
|
||||
if name.blank?
|
||||
return render json: { error: "Name is required" }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t("flash.accounts.name_required") }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if current_user.update(name: name)
|
||||
@@ -30,7 +30,7 @@ module Api
|
||||
return render json: { error: password_error_message(result.error) }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
render json: { message: "Password updated" }
|
||||
render json: { message: I18n.t("flash.accounts.password_updated") }
|
||||
end
|
||||
|
||||
private
|
||||
@@ -46,10 +46,13 @@ module Api
|
||||
|
||||
def password_error_message(code)
|
||||
case code
|
||||
when :current_incorrect then "Current password is incorrect"
|
||||
when :too_short then "Password must be at least 8 characters"
|
||||
when :mismatch then "Passwords do not match"
|
||||
else "Unable to update password"
|
||||
when :current_incorrect then I18n.t("flash.accounts.password_current_incorrect")
|
||||
when :too_short then I18n.t("flash.accounts.password_too_short")
|
||||
when :too_long then I18n.t("flash.accounts.password_too_long")
|
||||
when :too_weak then I18n.t("flash.accounts.password_too_weak")
|
||||
when :same_as_current then I18n.t("flash.accounts.password_same_as_current")
|
||||
when :mismatch then I18n.t("flash.accounts.password_mismatch")
|
||||
else I18n.t("flash.accounts.password_update_failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,18 +22,18 @@ module Api
|
||||
if user&.authenticate(params[:password])
|
||||
render json: token_response(user), status: :ok
|
||||
else
|
||||
render json: { error: "Invalid credentials" }, status: :unauthorized
|
||||
render json: { error: I18n.t("flash.sessions.invalid_credentials") }, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def logout
|
||||
render json: { message: "Logged out" }
|
||||
render json: { message: I18n.t("flash.sessions.logged_out") }
|
||||
end
|
||||
|
||||
def refresh
|
||||
payload = JsonWebToken.decode(params[:refresh_token] || bearer_token)
|
||||
user = User.find_by(id: payload&.dig(:user_id))
|
||||
return render json: { error: "Invalid token" }, status: :unauthorized unless user
|
||||
return render json: { error: I18n.t("flash.sessions.invalid_token") }, status: :unauthorized unless user
|
||||
|
||||
render json: token_response(user)
|
||||
end
|
||||
@@ -45,7 +45,7 @@ module Api
|
||||
def forgot_password
|
||||
Users::RequestPasswordReset.call(email: params[:email])
|
||||
render json: {
|
||||
message: "If the email is registered, you will receive a password reset link shortly."
|
||||
message: I18n.t("flash.password_resets.email_sent")
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
class ApplicationController < ActionController::API
|
||||
include ActionController::HttpAuthentication::Token::ControllerMethods
|
||||
|
||||
before_action :set_api_locale
|
||||
before_action :authenticate_request!
|
||||
|
||||
attr_reader :current_user
|
||||
|
||||
private
|
||||
|
||||
def set_api_locale
|
||||
explicit = LocaleResolver.normalize(request.headers["X-Locale"])
|
||||
I18n.locale = explicit ||
|
||||
LocaleResolver.from_accept_language(request.headers["Accept-Language"]) ||
|
||||
I18n.default_locale
|
||||
end
|
||||
|
||||
def authenticate_request!
|
||||
token = bearer_token
|
||||
payload = JsonWebToken.decode(token)
|
||||
@current_user = User.find_by(id: payload[:user_id]) if payload
|
||||
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
|
||||
render json: { error: I18n.t("flash.sessions.unauthorized") }, status: :unauthorized unless @current_user
|
||||
end
|
||||
|
||||
def bearer_token
|
||||
|
||||
@@ -29,8 +29,8 @@ module Public
|
||||
)
|
||||
|
||||
unless result.ok?
|
||||
flash.now[:alert] = t("flash.accounts.password_#{result.error}")
|
||||
return render :show, status: :unprocessable_entity
|
||||
redirect_to public_account_path, alert: t("flash.accounts.password_#{result.error}")
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to public_account_path, notice: t("flash.accounts.password_updated")
|
||||
|
||||
@@ -11,17 +11,8 @@ module Public
|
||||
load_club_billing_context_for_pricing
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_club_billing_context_for_pricing
|
||||
return unless logged_in?
|
||||
|
||||
@club = current_user.primary_club
|
||||
return unless @club
|
||||
|
||||
@subscription = @club.subscription
|
||||
@team = @club.teams.order(:name).first
|
||||
@entitlements = @team&.entitlements
|
||||
def support
|
||||
@app_store_review_chrome = true
|
||||
end
|
||||
|
||||
def privacy
|
||||
@@ -38,5 +29,18 @@ module Public
|
||||
|
||||
def pallavolo
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_club_billing_context_for_pricing
|
||||
return unless logged_in?
|
||||
|
||||
@club = current_user.primary_club
|
||||
return unless @club
|
||||
|
||||
@subscription = @club.subscription
|
||||
@team = @club.teams.order(:name).first
|
||||
@entitlements = @team&.entitlements
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ module Public
|
||||
def edit
|
||||
@user = User.find_by_password_reset_token(params[:token])
|
||||
if @user.nil? || @user.password_reset_expired?
|
||||
redirect_to new_public_password_reset_path,
|
||||
redirect_to public_password_forgot_path,
|
||||
alert: t("flash.password_resets.invalid_or_expired_link")
|
||||
return
|
||||
end
|
||||
@@ -23,19 +23,25 @@ module Public
|
||||
def update
|
||||
@user = User.find_by_password_reset_token(params[:token])
|
||||
if @user.nil? || @user.password_reset_expired?
|
||||
redirect_to new_public_password_reset_path,
|
||||
redirect_to public_password_forgot_path,
|
||||
alert: t("flash.password_resets.invalid_or_expired_link")
|
||||
return
|
||||
end
|
||||
|
||||
if params[:password].blank? || params[:password].length < 8
|
||||
flash.now[:alert] = t("flash.password_resets.password_min_length")
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("flash.password_resets.password_mismatch")
|
||||
@token = params[:token]
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("flash.password_resets.password_mismatch")
|
||||
if (code = PasswordComplexity.violation(params[:password]))
|
||||
flash.now[:alert] = t("flash.password_resets.password_#{code == :blank ? :too_short : code}")
|
||||
@token = params[:token]
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if PasswordComplexity.same_as_current?(@user, params[:password])
|
||||
flash.now[:alert] = t("flash.password_resets.password_same_as_current")
|
||||
@token = params[:token]
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@ module Public
|
||||
{ loc: "#{base}/live", changefreq: "hourly", priority: "0.85" },
|
||||
{ loc: "#{base}/squadre", changefreq: "daily", priority: "0.85" },
|
||||
{ loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" },
|
||||
{ loc: "#{base}/support", changefreq: "yearly", priority: "0.3" },
|
||||
{ loc: "#{base}/cookie", changefreq: "yearly", priority: "0.3" },
|
||||
{ loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" }
|
||||
]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class AdminAccount < ApplicationRecord
|
||||
include PasswordComplexity
|
||||
|
||||
has_secure_password
|
||||
|
||||
validates :username, presence: true, uniqueness: true
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Criteri "di mercato" (stile Cognito/Auth0 bilanciato):
|
||||
# - minimo 8 caratteri (max 72 per bcrypt)
|
||||
# - almeno 3 classi su 4: minuscole, maiuscole, numeri, simboli
|
||||
module PasswordComplexity
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
MIN_LENGTH = 8
|
||||
MAX_LENGTH = 72
|
||||
REQUIRED_CLASSES = 3
|
||||
|
||||
CLASS_CHECKS = {
|
||||
lowercase: /[a-z]/,
|
||||
uppercase: /[A-Z]/,
|
||||
digit: /\d/,
|
||||
symbol: /[^A-Za-z0-9]/
|
||||
}.freeze
|
||||
|
||||
class << self
|
||||
def violation(password)
|
||||
value = password.to_s
|
||||
return :blank if value.blank?
|
||||
return :too_short if value.length < MIN_LENGTH
|
||||
return :too_long if value.bytesize > MAX_LENGTH
|
||||
return :too_weak unless strong_enough?(value)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def strong_enough?(password)
|
||||
matched = CLASS_CHECKS.count { |_, pattern| password.match?(pattern) }
|
||||
matched >= REQUIRED_CLASSES
|
||||
end
|
||||
|
||||
# Confronta con il digest già salvato (prima di assegnare la nuova password).
|
||||
def same_as_current?(record, password)
|
||||
return false if password.blank? || !record.respond_to?(:authenticate)
|
||||
|
||||
record.authenticate(password).present?
|
||||
end
|
||||
|
||||
def requirement_summary
|
||||
I18n.t("password_policy.hint")
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
validate :password_meets_complexity_policy, if: -> { password.present? }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def password_meets_complexity_policy
|
||||
case PasswordComplexity.violation(password)
|
||||
when :too_short
|
||||
errors.add(:password, :too_short, count: MIN_LENGTH)
|
||||
when :too_long
|
||||
errors.add(:password, :too_long, count: MAX_LENGTH)
|
||||
when :too_weak
|
||||
errors.add(:password, :complexity)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
class User < ApplicationRecord
|
||||
include PasswordComplexity
|
||||
|
||||
ROLES = %w[admin coach parent volunteer].freeze
|
||||
|
||||
has_secure_password
|
||||
|
||||
@@ -23,15 +23,23 @@ module Users
|
||||
return Result.new(ok?: false, error: :current_incorrect)
|
||||
end
|
||||
|
||||
if @password.blank? || @password.length < 8
|
||||
return Result.new(ok?: false, error: :too_short)
|
||||
end
|
||||
|
||||
if @password != @password_confirmation
|
||||
return Result.new(ok?: false, error: :mismatch)
|
||||
end
|
||||
|
||||
@user.update!(password: @password)
|
||||
if (code = PasswordComplexity.violation(@password))
|
||||
return Result.new(ok?: false, error: code == :blank ? :too_short : code)
|
||||
end
|
||||
|
||||
if PasswordComplexity.same_as_current?(@user, @password)
|
||||
return Result.new(ok?: false, error: :same_as_current)
|
||||
end
|
||||
|
||||
unless @user.update(password: @password)
|
||||
complexity_error = @user.errors.details[:password]&.any? { |d| d[:error] == :complexity }
|
||||
return Result.new(ok?: false, error: complexity_error ? :too_weak : :too_short)
|
||||
end
|
||||
|
||||
@user.clear_password_reset!
|
||||
Result.new(ok?: true)
|
||||
end
|
||||
|
||||
@@ -8,17 +8,17 @@
|
||||
<%= render "shared/meta_tags" %>
|
||||
<%= yield :head %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<link rel="stylesheet" href="/marketing.css?v=44">
|
||||
<link rel="stylesheet" href="/marketing.css?v=50">
|
||||
</head>
|
||||
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
||||
<%= render "shared/cookie_banner" %>
|
||||
<%= render "shared/marketing_nav" %>
|
||||
<%= render(@app_store_review_chrome ? "shared/marketing_nav_app_store" : "shared/marketing_nav") %>
|
||||
<main>
|
||||
<% if flash[:notice] %><div class="wrap"><div class="flash notice"><%= flash[:notice] %></div></div><% end %>
|
||||
<% if flash[:alert] %><div class="wrap"><div class="flash alert"><%= flash[:alert] %></div></div><% end %>
|
||||
<%= yield %>
|
||||
</main>
|
||||
<%= render "shared/marketing_footer" %>
|
||||
<%= render(@app_store_review_chrome ? "shared/marketing_footer_app_store" : "shared/marketing_footer") %>
|
||||
<script src="/branding-form.js?v=1" defer></script>
|
||||
<script src="/roster-form.js?v=1" defer></script>
|
||||
<script src="/password-toggle.js?v=2" defer></script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
||||
<%= render "shared/meta_tags" %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<link rel="stylesheet" href="/marketing.css?v=44">
|
||||
<link rel="stylesheet" href="/marketing.css?v=50">
|
||||
<link rel="stylesheet" href="/live.css?v=26">
|
||||
<%= yield :head %>
|
||||
</head>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
<div class="card">
|
||||
<h2 style="font-size:1.1rem;margin-top:0"><%= t("auth.account.password_heading") %></h2>
|
||||
<p class="muted" style="font-size:0.9rem"><%= t("password_policy.hint") %></p>
|
||||
<%= form_with url: public_account_password_path, method: :patch, local: true do %>
|
||||
<%= render "shared/input_toggle",
|
||||
name: :current_password,
|
||||
|
||||
@@ -2,25 +2,27 @@
|
||||
<% content_for :meta_description, t("auth.invitation.meta_description") %>
|
||||
<% content_for :robots, "noindex, nofollow" %>
|
||||
|
||||
<div class="card" style="max-width:480px">
|
||||
<h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1>
|
||||
<p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p>
|
||||
<p class="muted" style="font-size:0.9rem;margin-bottom:16px">
|
||||
<%= raw t("auth.invitation.instructions_html", email: @invitation.email) %>
|
||||
</p>
|
||||
<% if logged_in? %>
|
||||
<%= button_to t("auth.invitation.accept"), public_invitation_path(token: @token), method: :post, class: "btn btn-primary" %>
|
||||
<% else %>
|
||||
<p><%= raw t(
|
||||
"auth.invitation.login_or_signup_html",
|
||||
login_link: link_to(t("auth.invitation.login_link"), public_login_path),
|
||||
signup_link: link_to(t("auth.invitation.signup_link"), public_signup_path),
|
||||
email: @invitation.email
|
||||
) %></p>
|
||||
<%= button_to t("auth.invitation.accept_if_logged_in"), public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %>
|
||||
<% end %>
|
||||
<p style="margin-top:16px;font-size:0.88rem;color:#888">
|
||||
<%= t("auth.invitation.mobile_app_label") %>
|
||||
<a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a>
|
||||
</p>
|
||||
</div>
|
||||
<section class="auth-page">
|
||||
<div class="card">
|
||||
<h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1>
|
||||
<p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p>
|
||||
<p class="muted" style="font-size:0.9rem;margin-bottom:16px">
|
||||
<%= raw t("auth.invitation.instructions_html", email: @invitation.email) %>
|
||||
</p>
|
||||
<% if logged_in? %>
|
||||
<%= button_to t("auth.invitation.accept"), public_invitation_path(token: @token), method: :post, class: "btn btn-primary" %>
|
||||
<% else %>
|
||||
<p><%= raw t(
|
||||
"auth.invitation.login_or_signup_html",
|
||||
login_link: link_to(t("auth.invitation.login_link"), public_login_path),
|
||||
signup_link: link_to(t("auth.invitation.signup_link"), public_signup_path),
|
||||
email: @invitation.email
|
||||
) %></p>
|
||||
<%= button_to t("auth.invitation.accept_if_logged_in"), public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %>
|
||||
<% end %>
|
||||
<p style="margin-top:16px;font-size:0.88rem;color:#888">
|
||||
<%= t("auth.invitation.mobile_app_label") %>
|
||||
<a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
|
||||
<h3><%= t("legal.cookies.s4_1_title") %></h3>
|
||||
<p><%= t("legal.cookies.s4_1_intro") %></p>
|
||||
<table class="legal-table">
|
||||
<div class="table-scroll">
|
||||
<table class="legal-table">
|
||||
<thead>
|
||||
<tr><th><%= t("legal.cookies.table_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
|
||||
</thead>
|
||||
@@ -64,7 +65,8 @@
|
||||
<td><%= t("legal.cookies.s4_1_row2_provider") %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3><%= t("legal.cookies.s4_2_title") %></h3>
|
||||
<p>
|
||||
@@ -75,7 +77,8 @@
|
||||
<% else %>
|
||||
<p class="muted"><%= t("legal.cookies.s4_2_inactive") %></p>
|
||||
<% end %>
|
||||
<table class="legal-table">
|
||||
<div class="table-scroll">
|
||||
<table class="legal-table">
|
||||
<thead>
|
||||
<tr><th><%= t("legal.cookies.table2_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
|
||||
</thead>
|
||||
@@ -99,7 +102,8 @@
|
||||
<td><%= t("legal.cookies.s4_2_row3_provider") %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
<%= raw t(
|
||||
"legal.cookies.s4_2_p2_html",
|
||||
|
||||
@@ -22,20 +22,22 @@
|
||||
|
||||
<%= render "shared/plan_cards" %>
|
||||
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><%= t("pages.pricing.table_staff") %></td><td>1</td><td>5</td><td><%= t("pages.pricing.table_unlimited") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_matches") %></td><td>1</td><td>3</td><td>10</td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_live_mltv") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_price") %></td><td><%= t("pages.pricing.table_price_free") %></td><td><%= raw t("pages.pricing.table_price_light_html") %></td><td><%= raw t("pages.pricing.table_price_full_html") %></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-scroll">
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><%= t("pages.pricing.table_staff") %></td><td>1</td><td>5</td><td><%= t("pages.pricing.table_unlimited") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_matches") %></td><td>1</td><td>3</td><td>10</td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_live_mltv") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
||||
<tr><td><%= t("pages.pricing.table_price") %></td><td><%= t("pages.pricing.table_price_free") %></td><td><%= raw t("pages.pricing.table_price_light_html") %></td><td><%= raw t("pages.pricing.table_price_full_html") %></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:32px;text-align:center">
|
||||
<h3 style="margin-top:0"><%= t("pages.pricing.different_title") %></h3>
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
|
||||
<section>
|
||||
<h2><%= t("legal.privacy.s5_title") %></h2>
|
||||
<table class="legal-table">
|
||||
<div class="table-scroll">
|
||||
<table class="legal-table">
|
||||
<thead>
|
||||
<tr><th><%= t("legal.privacy.s5_col_purpose") %></th><th><%= t("legal.privacy.s5_col_basis") %></th></tr>
|
||||
</thead>
|
||||
@@ -107,7 +108,8 @@
|
||||
<td><%= t("legal.privacy.s5_row6_basis") %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<% content_for :title, t("legal.support.title") %>
|
||||
<% content_for :meta_description, t("legal.support.meta_description") %>
|
||||
<% content_for :canonical_url, seo_absolute_url(public_support_path) %>
|
||||
|
||||
<div class="wrap legal-doc">
|
||||
<h1><%= t("legal.support.h1") %></h1>
|
||||
<p><%= t("legal.support.intro") %></p>
|
||||
|
||||
<p>
|
||||
<%= t("legal.support.email_label") %>
|
||||
<a href="mailto:<%= MatchLiveTv.support_email %>"><%= MatchLiveTv.support_email %></a>
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2><%= t("legal.support.access_title") %></h2>
|
||||
<p><%= t("legal.support.access_body") %></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><%= t("legal.support.live_title") %></h2>
|
||||
<p><%= t("legal.support.live_body") %></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><%= t("legal.support.team_title") %></h2>
|
||||
<p><%= t("legal.support.team_body") %></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><%= t("legal.support.privacy_title") %></h2>
|
||||
<p>
|
||||
<%= raw t(
|
||||
"legal.support.privacy_body_html",
|
||||
privacy_link: link_to(t("legal.support.privacy_link_text"), public_privacy_path)
|
||||
) %>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -4,6 +4,7 @@
|
||||
<section class="auth-page">
|
||||
<h1><%= t("auth.password_reset.title") %></h1>
|
||||
<div class="card">
|
||||
<p class="muted" style="font-size:0.9rem"><%= t("password_policy.hint") %></p>
|
||||
<%= form_with url: public_password_reset_path, method: :patch, local: true do %>
|
||||
<%= hidden_field_tag :token, @token %>
|
||||
<%= render "shared/input_toggle",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
required: true,
|
||||
minlength: 8,
|
||||
autocomplete: "new-password" %>
|
||||
<p class="muted" style="font-size:0.9rem;margin-top:-0.5rem"><%= t("password_policy.hint") %></p>
|
||||
<%= render "shared/input_toggle",
|
||||
name: "user[password_confirmation]",
|
||||
id: "user_password_confirmation",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
<h3 style="margin-top:28px;font-size:1.1rem"><%= t("billing.documents.table_heading") %></h3>
|
||||
<% if payments.any? %>
|
||||
<div class="table-scroll">
|
||||
<table class="data billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -60,6 +61,7 @@
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<p style="color:#888"><%= t("billing.documents.no_payments") %></p>
|
||||
<% end %>
|
||||
|
||||
@@ -58,10 +58,18 @@
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpen(menu.hidden);
|
||||
});
|
||||
|
||||
// Keep parent mobile nav open while interacting with the switcher UI.
|
||||
root.addEventListener("click", function (e) {
|
||||
if (e.target.closest(".lang-switcher__toggle")) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!root.contains(e.target)) setOpen(false);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<strong style="color:#fff">Match Live TV</strong> — <%= t("footer.tagline") %>
|
||||
</div>
|
||||
<div>
|
||||
<%= link_to t("common.support"), public_support_path %> ·
|
||||
<%= link_to t("common.pricing"), public_prezzi_path %> ·
|
||||
<%= link_to t("footer.live"), public_live_index_path %> ·
|
||||
<%= link_to t("common.faq"), public_faq_path %> ·
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<%# Footer minimale per App Store Review: solo link legali/supporto, nessun CTA commerciale. %>
|
||||
<footer class="site-footer">
|
||||
<div class="wrap">
|
||||
<div>
|
||||
<strong style="color:#fff">Match Live TV</strong> — <%= t("footer.tagline") %>
|
||||
</div>
|
||||
<div>
|
||||
<%= link_to t("common.support"), public_support_path %> ·
|
||||
<%= link_to t("common.privacy"), public_privacy_path %> ·
|
||||
<%= link_to t("common.cookies"), public_cookies_path %> ·
|
||||
<%= link_to t("common.terms"), public_termini_path %>
|
||||
· <button type="button" class="footer-link-btn" data-cookie-manage><%= t("footer.manage_cookies") %></button>
|
||||
</div>
|
||||
<div class="site-footer__legal">
|
||||
<p><%= t("footer.copyright") %></p>
|
||||
<p><%= t("footer.responsibility") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -19,6 +19,15 @@
|
||||
|
||||
<nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true">
|
||||
<div class="nav-panel">
|
||||
<div class="nav-mobile-head">
|
||||
<%= link_to root_path, class: "nav-mobile-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
|
||||
<img class="nav-mobile-brand-logo" src="/logo.png?v=3" alt="" width="40" height="40" decoding="async">
|
||||
<span class="brand">Match <span>Live TV</span></span>
|
||||
<% end %>
|
||||
<div class="nav-lang">
|
||||
<%= render "shared/language_switcher" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= link_to t("nav.home"), root_path, class: (request.path == "/" ? "nav-active" : nil) %>
|
||||
<%= link_to t("nav.features"), public_features_path, class: (request.path == "/funzionalita" ? "nav-active" : nil) %>
|
||||
<%= link_to t("nav.pricing"), public_prezzi_path, class: (request.path == "/prezzi" ? "nav-active" : nil) %>
|
||||
@@ -40,9 +49,6 @@
|
||||
<%= link_to t("nav.login"), public_login_path, class: "nav-link-item" %>
|
||||
<%= link_to t("nav.signup"), public_signup_path, class: "btn btn-primary nav-btn" %>
|
||||
<% end %>
|
||||
<div class="nav-lang">
|
||||
<%= render "shared/language_switcher" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -78,8 +84,16 @@
|
||||
|
||||
if (backdrop) backdrop.addEventListener("click", closeMenu);
|
||||
|
||||
function shouldCloseNavOnControl(el) {
|
||||
// Language toggle must keep the mobile menu open; only a locale choice may close it.
|
||||
if (!el.closest("[data-lang-switcher]")) return true;
|
||||
return el.classList.contains("lang-switcher__option");
|
||||
}
|
||||
|
||||
nav.querySelectorAll("a, button").forEach(function (el) {
|
||||
el.addEventListener("click", closeMenu);
|
||||
el.addEventListener("click", function () {
|
||||
if (shouldCloseNavOnControl(el)) closeMenu();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<%# Chrome minimale per App Store Review: branding, lingua, Privacy e Supporto — senza CTA commerciali. %>
|
||||
<div class="site-chrome">
|
||||
<div class="site-masthead">
|
||||
<div class="wrap mast-inner">
|
||||
<%= link_to public_support_path, class: "mast-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
|
||||
<img class="mast-brand-logo" src="/logo.png?v=3" alt="Match Live TV" width="40" height="40" decoding="async">
|
||||
<span class="brand" aria-hidden="true">Match <span>Live TV</span></span>
|
||||
<% end %>
|
||||
|
||||
<div class="mast-tools">
|
||||
<button type="button" class="nav-toggle" aria-label="<%= t('nav.open_menu') %>" aria-expanded="false" aria-controls="site-nav">
|
||||
<span class="nav-toggle-bar" aria-hidden="true"></span>
|
||||
<span class="nav-toggle-bar" aria-hidden="true"></span>
|
||||
<span class="nav-toggle-bar" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true">
|
||||
<div class="nav-panel">
|
||||
<div class="nav-mobile-head">
|
||||
<%= link_to public_support_path, class: "nav-mobile-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
|
||||
<img class="nav-mobile-brand-logo" src="/logo.png?v=3" alt="" width="40" height="40" decoding="async">
|
||||
<span class="brand">Match <span>Live TV</span></span>
|
||||
<% end %>
|
||||
<div class="nav-lang">
|
||||
<%= render "shared/language_switcher" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= link_to t("common.support"), public_support_path, class: "nav-active" %>
|
||||
<%= link_to t("common.privacy"), public_privacy_path, class: (request.path == "/privacy" ? "nav-active" : nil) %>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="nav-backdrop" id="nav-backdrop" aria-hidden="true"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var chrome = document.querySelector(".site-chrome");
|
||||
var toggle = document.querySelector(".nav-toggle");
|
||||
var backdrop = document.getElementById("nav-backdrop");
|
||||
var nav = document.getElementById("site-nav");
|
||||
if (!chrome || !toggle || !nav) return;
|
||||
var openLabel = <%= raw t("nav.open_menu").to_json %>;
|
||||
var closeLabel = <%= raw t("nav.close_menu").to_json %>;
|
||||
|
||||
function setOpen(open) {
|
||||
chrome.classList.toggle("nav-open", open);
|
||||
document.body.classList.toggle("nav-menu-open", open);
|
||||
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
toggle.setAttribute("aria-label", open ? closeLabel : openLabel);
|
||||
nav.setAttribute("aria-hidden", open ? "false" : "true");
|
||||
if (backdrop) backdrop.setAttribute("aria-hidden", open ? "false" : "true");
|
||||
}
|
||||
|
||||
function closeMenu() { setOpen(false); }
|
||||
|
||||
toggle.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
document.body.classList.contains("nav-menu-open") ? closeMenu() : setOpen(true);
|
||||
});
|
||||
|
||||
if (backdrop) backdrop.addEventListener("click", closeMenu);
|
||||
|
||||
function shouldCloseNavOnControl(el) {
|
||||
if (!el.closest("[data-lang-switcher]")) return true;
|
||||
return el.classList.contains("lang-switcher__option");
|
||||
}
|
||||
|
||||
nav.querySelectorAll("a, button").forEach(function (el) {
|
||||
el.addEventListener("click", function () {
|
||||
if (shouldCloseNavOnControl(el)) closeMenu();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.matchMedia("(min-width: 900px)").matches) closeMenu();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") closeMenu();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -81,6 +81,12 @@ module MatchLiveTv
|
||||
ENV.fetch("PRIVACY_CONTACT_EMAIL", "privacy@matchlivetv.it")
|
||||
end
|
||||
|
||||
# Email di supporto (App Store Support URL e contatti assistenza).
|
||||
# Preferisce SUPPORT_CONTACT_EMAIL; default = contatto commerciale già usato nel sito.
|
||||
def support_email
|
||||
ENV["SUPPORT_CONTACT_EMAIL"].presence || "info@matchlivetv.it"
|
||||
end
|
||||
|
||||
def privacy_controller_address
|
||||
ENV.fetch("PRIVACY_CONTROLLER_ADDRESS", "Via Guido De Ruggiero, 89 - 20142 - Milano (MI)")
|
||||
end
|
||||
|
||||
@@ -18,6 +18,9 @@ de:
|
||||
logout_success: Abgemeldet
|
||||
password_current_incorrect: Das aktuelle Passwort ist falsch
|
||||
password_too_short: Das neue Passwort muss mindestens 8 Zeichen lang sein
|
||||
password_too_long: Das neue Passwort darf höchstens 72 Zeichen lang sein
|
||||
password_too_weak: Das neue Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
|
||||
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
|
||||
password_mismatch: Die Passwörter stimmen nicht überein
|
||||
password_updated: Passwort aktualisiert
|
||||
ops_acknowledged: Vorfall übernommen
|
||||
@@ -53,7 +56,7 @@ de:
|
||||
edit:
|
||||
title: Passwort ändern
|
||||
current_password_label: Aktuelles Passwort
|
||||
new_password_label: "Neues Passwort (mind. 8 Zeichen)"
|
||||
new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
|
||||
confirm_password_label: Neues Passwort bestätigen
|
||||
submit: Passwort speichern
|
||||
cancel: Abbrechen
|
||||
|
||||
@@ -18,6 +18,9 @@ en:
|
||||
logout_success: Signed out
|
||||
password_current_incorrect: Current password is incorrect
|
||||
password_too_short: The new password must be at least 8 characters long
|
||||
password_too_long: New password cannot exceed 72 characters
|
||||
password_too_weak: "New password must include at least 3 of: lowercase, uppercase, numbers and symbols"
|
||||
password_same_as_current: New password must be different from the current password
|
||||
password_mismatch: Passwords do not match
|
||||
password_updated: Password updated
|
||||
ops_acknowledged: Incident acknowledged
|
||||
@@ -53,7 +56,7 @@ en:
|
||||
edit:
|
||||
title: Change password
|
||||
current_password_label: Current password
|
||||
new_password_label: "New password (min. 8 characters)"
|
||||
new_password_label: "New password (min. 8, at least 3 character types)"
|
||||
confirm_password_label: Confirm new password
|
||||
submit: Save password
|
||||
cancel: Cancel
|
||||
|
||||
@@ -18,6 +18,9 @@ es:
|
||||
logout_success: Sesión cerrada
|
||||
password_current_incorrect: La contraseña actual no es correcta
|
||||
password_too_short: La nueva contraseña debe tener al menos 8 caracteres
|
||||
password_too_long: La nueva contraseña no puede superar los 72 caracteres
|
||||
password_too_weak: "La nueva contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||
password_same_as_current: La nueva contraseña debe ser distinta de la actual
|
||||
password_mismatch: Las contraseñas no coinciden
|
||||
password_updated: Contraseña actualizada
|
||||
ops_acknowledged: Incidencia asumida
|
||||
@@ -53,7 +56,7 @@ es:
|
||||
edit:
|
||||
title: Cambiar contraseña
|
||||
current_password_label: Contraseña actual
|
||||
new_password_label: "Nueva contraseña (mín. 8 caracteres)"
|
||||
new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
|
||||
confirm_password_label: Confirma la nueva contraseña
|
||||
submit: Guardar contraseña
|
||||
cancel: Cancelar
|
||||
|
||||
@@ -18,6 +18,9 @@ fr:
|
||||
logout_success: Déconnecté
|
||||
password_current_incorrect: Le mot de passe actuel est incorrect
|
||||
password_too_short: Le nouveau mot de passe doit contenir au moins 8 caractères
|
||||
password_too_long: Le nouveau mot de passe ne peut pas dépasser 72 caractères
|
||||
password_too_weak: "Le nouveau mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
|
||||
password_mismatch: Les mots de passe ne correspondent pas
|
||||
password_updated: Mot de passe mis à jour
|
||||
ops_acknowledged: Incident pris en charge
|
||||
@@ -53,7 +56,7 @@ fr:
|
||||
edit:
|
||||
title: Changer le mot de passe
|
||||
current_password_label: Mot de passe actuel
|
||||
new_password_label: "Nouveau mot de passe (min. 8 caractères)"
|
||||
new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
|
||||
confirm_password_label: Confirmer le nouveau mot de passe
|
||||
submit: Enregistrer le mot de passe
|
||||
cancel: Annuler
|
||||
|
||||
@@ -18,6 +18,9 @@ it:
|
||||
logout_success: Disconnesso
|
||||
password_current_incorrect: Password attuale non corretta
|
||||
password_too_short: La nuova password deve avere almeno 8 caratteri
|
||||
password_too_long: La nuova password non può superare i 72 caratteri
|
||||
password_too_weak: "La nuova password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||
password_same_as_current: La nuova password deve essere diversa da quella attuale
|
||||
password_mismatch: Le password non coincidono
|
||||
password_updated: Password aggiornata
|
||||
ops_acknowledged: Incidente preso in carico
|
||||
@@ -53,7 +56,7 @@ it:
|
||||
edit:
|
||||
title: Cambia password
|
||||
current_password_label: Password attuale
|
||||
new_password_label: "Nuova password (min. 8 caratteri)"
|
||||
new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
|
||||
confirm_password_label: Conferma nuova password
|
||||
submit: Salva password
|
||||
cancel: Annulla
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
de:
|
||||
password_policy:
|
||||
hint: "Mindestens 8 Zeichen, mit mindestens 3 aus: Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen."
|
||||
activerecord:
|
||||
attributes:
|
||||
user:
|
||||
password: Passwort
|
||||
admin_account:
|
||||
password: Passwort
|
||||
errors:
|
||||
models:
|
||||
user:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "ist zu kurz (mindestens %{count} Zeichen)"
|
||||
too_long: "ist zu lang (höchstens %{count} Zeichen)"
|
||||
complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
|
||||
admin_account:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "ist zu kurz (mindestens %{count} Zeichen)"
|
||||
too_long: "ist zu lang (höchstens %{count} Zeichen)"
|
||||
complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
|
||||
club:
|
||||
back_to_club: "← Verein"
|
||||
sport_label: Hauptsportart
|
||||
@@ -600,10 +622,16 @@ de:
|
||||
welcome_back: Willkommen zurück!
|
||||
invalid_credentials: E-Mail oder Passwort ungültig
|
||||
logged_out: Abgemeldet
|
||||
unauthorized: Nicht autorisiert
|
||||
invalid_token: Ungültiges Token
|
||||
password_resets:
|
||||
email_sent: Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen des Passworts.
|
||||
invalid_or_expired_link: Link ungültig oder abgelaufen. Fordere ein neues Zurücksetzen des Passworts an.
|
||||
password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein
|
||||
password_too_short: Das Passwort muss mindestens 8 Zeichen lang sein
|
||||
password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
|
||||
password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
|
||||
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
|
||||
password_mismatch: Die Passwörter stimmen nicht überein
|
||||
password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden.
|
||||
accounts:
|
||||
@@ -611,8 +639,12 @@ de:
|
||||
profile_updated: Profil aktualisiert.
|
||||
password_current_incorrect: Das aktuelle Passwort ist falsch
|
||||
password_too_short: Das Passwort muss mindestens 8 Zeichen haben
|
||||
password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
|
||||
password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
|
||||
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
|
||||
password_mismatch: Die Passwörter stimmen nicht überein
|
||||
password_updated: Passwort aktualisiert.
|
||||
password_update_failed: Passwort konnte nicht aktualisiert werden
|
||||
replay:
|
||||
download_unavailable: Download nicht verfügbar
|
||||
not_available: Replay nicht verfügbar
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
en:
|
||||
password_policy:
|
||||
hint: "At least 8 characters, including 3 of: lowercase, uppercase, numbers and symbols."
|
||||
activerecord:
|
||||
errors:
|
||||
models:
|
||||
user:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "is too short (minimum is %{count} characters)"
|
||||
too_long: "is too long (maximum is %{count} characters)"
|
||||
complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
|
||||
admin_account:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "is too short (minimum is %{count} characters)"
|
||||
too_long: "is too long (maximum is %{count} characters)"
|
||||
complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
|
||||
club:
|
||||
back_to_club: "← Club"
|
||||
sport_label: Main sport
|
||||
@@ -600,10 +617,16 @@ en:
|
||||
welcome_back: Welcome back!
|
||||
invalid_credentials: Invalid email or password
|
||||
logged_out: Logged out
|
||||
unauthorized: Unauthorized
|
||||
invalid_token: Invalid token
|
||||
password_resets:
|
||||
email_sent: If the email is registered, you'll receive a password reset link shortly.
|
||||
invalid_or_expired_link: Invalid or expired link. Request a new password reset.
|
||||
password_min_length: Password must be at least 8 characters
|
||||
password_too_short: Password must be at least 8 characters
|
||||
password_too_long: Password cannot exceed 72 characters
|
||||
password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
|
||||
password_same_as_current: New password must be different from the current password
|
||||
password_mismatch: Passwords don't match
|
||||
password_updated: Password updated. You can now log in.
|
||||
accounts:
|
||||
@@ -611,8 +634,12 @@ en:
|
||||
profile_updated: Profile updated.
|
||||
password_current_incorrect: Current password is incorrect
|
||||
password_too_short: Password must be at least 8 characters
|
||||
password_too_long: Password cannot exceed 72 characters
|
||||
password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
|
||||
password_same_as_current: New password must be different from the current password
|
||||
password_mismatch: Passwords do not match
|
||||
password_updated: Password updated.
|
||||
password_update_failed: Unable to update password
|
||||
replay:
|
||||
download_unavailable: Download not available
|
||||
not_available: Replay not available
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
es:
|
||||
password_policy:
|
||||
hint: "Mínimo 8 caracteres, con al menos 3 entre: minúsculas, mayúsculas, números y símbolos."
|
||||
activerecord:
|
||||
attributes:
|
||||
user:
|
||||
password: Contraseña
|
||||
admin_account:
|
||||
password: Contraseña
|
||||
errors:
|
||||
models:
|
||||
user:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "es demasiado corta (mínimo %{count} caracteres)"
|
||||
too_long: "es demasiado larga (máximo %{count} caracteres)"
|
||||
complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||
admin_account:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "es demasiado corta (mínimo %{count} caracteres)"
|
||||
too_long: "es demasiado larga (máximo %{count} caracteres)"
|
||||
complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||
club:
|
||||
back_to_club: "← Club"
|
||||
sport_label: Deporte principal
|
||||
@@ -600,10 +622,16 @@ es:
|
||||
welcome_back: "¡Bienvenido de nuevo!"
|
||||
invalid_credentials: Correo o contraseña no válidos
|
||||
logged_out: Sesión cerrada
|
||||
unauthorized: No autorizado
|
||||
invalid_token: Token no válido
|
||||
password_resets:
|
||||
email_sent: Si el correo está registrado, recibirás en breve un enlace para restablecer la contraseña.
|
||||
invalid_or_expired_link: Enlace no válido o caducado. Solicita un nuevo restablecimiento de contraseña.
|
||||
password_min_length: La contraseña debe tener al menos 8 caracteres
|
||||
password_too_short: La contraseña debe tener al menos 8 caracteres
|
||||
password_too_long: La contraseña no puede superar los 72 caracteres
|
||||
password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||
password_same_as_current: La nueva contraseña debe ser distinta de la actual
|
||||
password_mismatch: Las contraseñas no coinciden
|
||||
password_updated: Contraseña actualizada. Ya puedes iniciar sesión.
|
||||
accounts:
|
||||
@@ -611,8 +639,12 @@ es:
|
||||
profile_updated: Perfil actualizado.
|
||||
password_current_incorrect: La contraseña actual no es correcta
|
||||
password_too_short: La contraseña debe tener al menos 8 caracteres
|
||||
password_too_long: La contraseña no puede superar los 72 caracteres
|
||||
password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||
password_same_as_current: La nueva contraseña debe ser distinta de la actual
|
||||
password_mismatch: Las contraseñas no coinciden
|
||||
password_updated: Contraseña actualizada.
|
||||
password_update_failed: No se pudo actualizar la contraseña
|
||||
replay:
|
||||
download_unavailable: Descarga no disponible
|
||||
not_available: Repetición no disponible
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
fr:
|
||||
password_policy:
|
||||
hint: "Au moins 8 caractères, avec au moins 3 parmi : minuscules, majuscules, chiffres et symboles."
|
||||
activerecord:
|
||||
attributes:
|
||||
user:
|
||||
password: Mot de passe
|
||||
admin_account:
|
||||
password: Mot de passe
|
||||
errors:
|
||||
models:
|
||||
user:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "est trop court (minimum %{count} caractères)"
|
||||
too_long: "est trop long (maximum %{count} caractères)"
|
||||
complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||
admin_account:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "est trop court (minimum %{count} caractères)"
|
||||
too_long: "est trop long (maximum %{count} caractères)"
|
||||
complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||
club:
|
||||
back_to_club: "← Club"
|
||||
sport_label: Sport principal
|
||||
@@ -600,10 +622,16 @@ fr:
|
||||
welcome_back: Bon retour !
|
||||
invalid_credentials: E-mail ou mot de passe invalide
|
||||
logged_out: Déconnecté
|
||||
unauthorized: Non autorisé
|
||||
invalid_token: Jeton invalide
|
||||
password_resets:
|
||||
email_sent: Si l'e-mail est enregistré, tu recevras bientôt un lien pour réinitialiser le mot de passe.
|
||||
invalid_or_expired_link: Lien invalide ou expiré. Demande une nouvelle réinitialisation du mot de passe.
|
||||
password_min_length: Le mot de passe doit comporter au moins 8 caractères
|
||||
password_too_short: Le mot de passe doit comporter au moins 8 caractères
|
||||
password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
|
||||
password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
|
||||
password_mismatch: Les mots de passe ne correspondent pas
|
||||
password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter.
|
||||
accounts:
|
||||
@@ -611,8 +639,12 @@ fr:
|
||||
profile_updated: Profil mis à jour.
|
||||
password_current_incorrect: Le mot de passe actuel est incorrect
|
||||
password_too_short: Le mot de passe doit contenir au moins 8 caractères
|
||||
password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
|
||||
password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
|
||||
password_mismatch: Les mots de passe ne correspondent pas
|
||||
password_updated: Mot de passe mis à jour.
|
||||
password_update_failed: Impossible de mettre à jour le mot de passe
|
||||
replay:
|
||||
download_unavailable: Téléchargement non disponible
|
||||
not_available: Replay non disponible
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
it:
|
||||
password_policy:
|
||||
hint: "Minimo 8 caratteri, con almeno 3 tra: minuscole, maiuscole, numeri e simboli."
|
||||
activerecord:
|
||||
errors:
|
||||
models:
|
||||
user:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "è troppo corta (minimo %{count} caratteri)"
|
||||
too_long: "è troppo lunga (massimo %{count} caratteri)"
|
||||
complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
|
||||
admin_account:
|
||||
attributes:
|
||||
password:
|
||||
too_short: "è troppo corta (minimo %{count} caratteri)"
|
||||
too_long: "è troppo lunga (massimo %{count} caratteri)"
|
||||
complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
|
||||
club:
|
||||
back_to_club: "← Società"
|
||||
sport_label: Sport principale
|
||||
@@ -600,10 +617,19 @@ it:
|
||||
welcome_back: Bentornato!
|
||||
invalid_credentials: Email o password non validi
|
||||
logged_out: Disconnesso
|
||||
unauthorized: Non autorizzato
|
||||
invalid_token: Token non valido
|
||||
password_resets:
|
||||
email_sent: Se l'email è registrata, riceverai a breve un link per reimpostare la password.
|
||||
invalid_or_expired_link: Link non valido o scaduto. Richiedi un nuovo reset password.
|
||||
password_min_length: La password deve avere almeno 8 caratteri
|
||||
password_too_short: La password deve avere almeno 8 caratteri
|
||||
password_too_long: La password non può superare i 72 caratteri
|
||||
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||
password_same_as_current: La nuova password deve essere diversa da quella attuale
|
||||
password_too_short: La password deve avere almeno 8 caratteri
|
||||
password_too_long: La password non può superare i 72 caratteri
|
||||
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||
password_mismatch: Le password non coincidono
|
||||
password_updated: Password aggiornata. Ora puoi accedere.
|
||||
accounts:
|
||||
@@ -611,8 +637,14 @@ it:
|
||||
profile_updated: Profilo aggiornato.
|
||||
password_current_incorrect: La password attuale non è corretta
|
||||
password_too_short: La password deve avere almeno 8 caratteri
|
||||
password_too_long: La password non può superare i 72 caratteri
|
||||
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||
password_same_as_current: La nuova password deve essere diversa da quella attuale
|
||||
password_too_long: La password non può superare i 72 caratteri
|
||||
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||
password_mismatch: Le password non coincidono
|
||||
password_updated: Password aggiornata.
|
||||
password_update_failed: Impossibile aggiornare la password
|
||||
replay:
|
||||
download_unavailable: Download non disponibile
|
||||
not_available: Replay non disponibile
|
||||
|
||||
@@ -195,3 +195,18 @@ de:
|
||||
s6_body_html: "Um die von der DSGVO vorgesehenen Rechte auszuüben (Zugang, Löschung, Widerspruch, Widerruf der Einwilligung), schreiben Sie an %{email_link}. Details finden Sie im %{privacy_doc_link}."
|
||||
s6_privacy_doc_link_text: Datenschutzdokument
|
||||
manage_button: Cookie-Einstellungen verwalten
|
||||
support:
|
||||
title: Match Live TV Support
|
||||
meta_description: "Match Live TV Hilfe: Zugangsprobleme, Livestreams, Teamverwaltung und Support-Kontaktdaten."
|
||||
h1: Match Live TV Support
|
||||
intro: "Brauchen Sie Hilfe mit Match Live TV? Bei Problemen mit dem Zugang, der Einrichtung von Livestreams, der Spielverwaltung oder der Nutzung der App können Sie unseren Support kontaktieren."
|
||||
email_label: "Support-E-Mail:"
|
||||
access_title: Zugangsprobleme
|
||||
access_body: "Wenn Sie sich nicht anmelden können, prüfen Sie die Zugangsdaten, die Sie von Ihrem Sportverein erhalten haben. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support."
|
||||
live_title: Probleme während eines Livestreams
|
||||
live_body: "Prüfen Sie Ihre Internetverbindung und versuchen Sie erneut, die Übertragung in der App zu starten. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support und geben Sie Gerät, App-Version und eine kurze Beschreibung des Problems an."
|
||||
team_title: Teamverwaltung
|
||||
team_body: "Konten und Übertragungsberechtigungen werden vom Sportverein verwaltet."
|
||||
privacy_title: Datenschutz
|
||||
privacy_body_html: "Informationen zur Verarbeitung personenbezogener Daten finden Sie auf der Seite %{privacy_link}."
|
||||
privacy_link_text: Datenschutz
|
||||
|
||||
@@ -195,3 +195,18 @@ en:
|
||||
s6_body_html: "To exercise the rights provided by the GDPR (access, erasure, objection, withdrawal of consent) write to %{email_link}. Details are in the %{privacy_doc_link}."
|
||||
s6_privacy_doc_link_text: privacy document
|
||||
manage_button: Manage cookie preferences
|
||||
support:
|
||||
title: Match Live TV Support
|
||||
meta_description: "Match Live TV help: access issues, live streaming, team management and support contact details."
|
||||
h1: Match Live TV Support
|
||||
intro: "Need help with Match Live TV? For access issues, live stream setup, match management or app usage, you can contact our support team."
|
||||
email_label: "Support email:"
|
||||
access_title: Access issues
|
||||
access_body: "If you cannot sign in, check the credentials provided by your sports club. If the problem persists, contact support."
|
||||
live_title: Issues during a live stream
|
||||
live_body: "Check your Internet connection and try starting the broadcast from the app again. If the problem persists, contact support and include your device, app version and a short description of the issue."
|
||||
team_title: Team management
|
||||
team_body: "Accounts and broadcasting permissions are managed by the sports club."
|
||||
privacy_title: Privacy
|
||||
privacy_body_html: "For information on personal data processing, see the %{privacy_link} page."
|
||||
privacy_link_text: Privacy
|
||||
|
||||
@@ -195,3 +195,18 @@ es:
|
||||
s6_body_html: "Para ejercer los derechos previstos por el RGPD (acceso, supresión, oposición, revocación del consentimiento) escribe a %{email_link}. Más detalles en el %{privacy_doc_link}."
|
||||
s6_privacy_doc_link_text: documento de privacidad
|
||||
manage_button: Gestionar preferencias de cookies
|
||||
support:
|
||||
title: Soporte Match Live TV
|
||||
meta_description: "Ayuda de Match Live TV: problemas de acceso, directos, gestión del equipo y datos de contacto del soporte."
|
||||
h1: Soporte Match Live TV
|
||||
intro: "¿Necesitas ayuda con Match Live TV? Para problemas de acceso, configuración de directos, gestión de partidos o uso de la app puedes contactar con nuestro soporte."
|
||||
email_label: "Correo de soporte:"
|
||||
access_title: Problemas de acceso
|
||||
access_body: "Si no puedes acceder, verifica las credenciales recibidas de tu club deportivo. Si el problema continúa, contacta con el soporte."
|
||||
live_title: Problemas durante un directo
|
||||
live_body: "Verifica la conexión a Internet e intenta de nuevo iniciar la transmisión desde la app. Si el problema continúa, contacta con el soporte indicando el dispositivo, la versión de la app y una breve descripción del problema."
|
||||
team_title: Gestión del equipo
|
||||
team_body: "Las cuentas y los permisos de transmisión los gestiona el club deportivo."
|
||||
privacy_title: Privacidad
|
||||
privacy_body_html: "Para información sobre el tratamiento de datos personales, consulta la página %{privacy_link}."
|
||||
privacy_link_text: Privacidad
|
||||
|
||||
@@ -195,3 +195,18 @@ fr:
|
||||
s6_body_html: "Pour exercer les droits prévus par le RGPD (accès, effacement, opposition, retrait du consentement), écrivez à %{email_link}. Détails dans le %{privacy_doc_link}."
|
||||
s6_privacy_doc_link_text: document de confidentialité
|
||||
manage_button: Gérer les préférences de cookies
|
||||
support:
|
||||
title: Support Match Live TV
|
||||
meta_description: "Assistance Match Live TV : problèmes d'accès, directs, gestion d'équipe et coordonnées du support."
|
||||
h1: Support Match Live TV
|
||||
intro: "Besoin d'aide avec Match Live TV ? Pour les problèmes d'accès, la configuration des directs, la gestion des matchs ou l'utilisation de l'application, vous pouvez contacter notre support."
|
||||
email_label: "E-mail du support :"
|
||||
access_title: Problèmes d'accès
|
||||
access_body: "Si vous ne parvenez pas à vous connecter, vérifiez les identifiants fournis par votre club sportif. Si le problème persiste, contactez le support."
|
||||
live_title: Problèmes pendant un direct
|
||||
live_body: "Vérifiez votre connexion Internet et réessayez de démarrer la diffusion depuis l'application. Si le problème persiste, contactez le support en indiquant l'appareil, la version de l'application et une brève description du problème."
|
||||
team_title: Gestion de l'équipe
|
||||
team_body: "Les comptes et les autorisations de diffusion sont gérés par le club sportif."
|
||||
privacy_title: Confidentialité
|
||||
privacy_body_html: "Pour les informations sur le traitement des données personnelles, consultez la page %{privacy_link}."
|
||||
privacy_link_text: Confidentialité
|
||||
|
||||
@@ -195,3 +195,18 @@ it:
|
||||
s6_body_html: "Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso) scrivi a %{email_link}. Dettagli nel %{privacy_doc_link}."
|
||||
s6_privacy_doc_link_text: documento privacy
|
||||
manage_button: Gestisci preferenze cookie
|
||||
support:
|
||||
title: Supporto Match Live TV
|
||||
meta_description: "Assistenza Match Live TV: problemi di accesso, dirette live, gestione squadra e contatti del supporto."
|
||||
h1: Supporto Match Live TV
|
||||
intro: "Hai bisogno di assistenza con Match Live TV? Per problemi di accesso, configurazione delle dirette, gestione delle partite o utilizzo dell’app puoi contattare il nostro supporto."
|
||||
email_label: "Email di supporto:"
|
||||
access_title: Problemi di accesso
|
||||
access_body: "Se non riesci ad accedere, verifica le credenziali ricevute dalla tua società sportiva. Se il problema persiste, contatta il supporto."
|
||||
live_title: Problemi durante una diretta
|
||||
live_body: "Verifica la connessione Internet e riprova ad avviare la trasmissione dall’app. Se il problema persiste, contatta il supporto indicando dispositivo, versione dell’app e una breve descrizione del problema."
|
||||
team_title: Gestione della squadra
|
||||
team_body: "Gli account e le autorizzazioni per la trasmissione sono gestiti dalla società sportiva."
|
||||
privacy_title: Privacy
|
||||
privacy_body_html: "Per informazioni sul trattamento dei dati personali consulta la pagina %{privacy_link}."
|
||||
privacy_link_text: Privacy
|
||||
|
||||
@@ -103,7 +103,7 @@ de:
|
||||
password_reset:
|
||||
meta_title: "Neues Passwort — Match Live TV"
|
||||
title: Neues Passwort wählen
|
||||
new_password_label: "Neues Passwort (min. 8 Zeichen)"
|
||||
new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
|
||||
submit: Passwort speichern
|
||||
back_to_login: Zurück zur Anmeldung
|
||||
invitation:
|
||||
@@ -128,11 +128,12 @@ de:
|
||||
name_label: Name
|
||||
role_label: "Rolle: %{role}"
|
||||
current_password_label: Aktuelles Passwort
|
||||
new_password_label: "Neues Passwort (mind. 8 Zeichen)"
|
||||
new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
|
||||
save_profile: Profil speichern
|
||||
save_password: Passwort aktualisieren
|
||||
common:
|
||||
privacy: Datenschutz
|
||||
support: Support
|
||||
cookies: Cookies
|
||||
terms: AGB
|
||||
pricing: Preise
|
||||
|
||||
@@ -103,7 +103,7 @@ en:
|
||||
password_reset:
|
||||
meta_title: "New password — Match Live TV"
|
||||
title: Choose a new password
|
||||
new_password_label: "New password (min. 8 characters)"
|
||||
new_password_label: "New password (min. 8, at least 3 character types)"
|
||||
submit: Save password
|
||||
back_to_login: Back to login
|
||||
invitation:
|
||||
@@ -128,11 +128,12 @@ en:
|
||||
name_label: Name
|
||||
role_label: "Role: %{role}"
|
||||
current_password_label: Current password
|
||||
new_password_label: "New password (min. 8 characters)"
|
||||
new_password_label: "New password (min. 8, at least 3 character types)"
|
||||
save_profile: Save profile
|
||||
save_password: Update password
|
||||
common:
|
||||
privacy: Privacy
|
||||
support: Support
|
||||
cookies: Cookies
|
||||
terms: Terms
|
||||
pricing: Pricing
|
||||
|
||||
@@ -103,7 +103,7 @@ es:
|
||||
password_reset:
|
||||
meta_title: "Nueva contraseña — Match Live TV"
|
||||
title: Elige una nueva contraseña
|
||||
new_password_label: "Nueva contraseña (mín. 8 caracteres)"
|
||||
new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
|
||||
submit: Guardar contraseña
|
||||
back_to_login: Volver al inicio de sesión
|
||||
invitation:
|
||||
@@ -128,11 +128,12 @@ es:
|
||||
name_label: Nombre
|
||||
role_label: "Rol: %{role}"
|
||||
current_password_label: Contraseña actual
|
||||
new_password_label: "Nueva contraseña (mín. 8 caracteres)"
|
||||
new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
|
||||
save_profile: Guardar perfil
|
||||
save_password: Actualizar contraseña
|
||||
common:
|
||||
privacy: Privacidad
|
||||
support: Soporte
|
||||
cookies: Cookies
|
||||
terms: Términos
|
||||
pricing: Precios
|
||||
|
||||
@@ -103,7 +103,7 @@ fr:
|
||||
password_reset:
|
||||
meta_title: "Nouveau mot de passe — Match Live TV"
|
||||
title: Choisissez un nouveau mot de passe
|
||||
new_password_label: "Nouveau mot de passe (8 caractères min.)"
|
||||
new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
|
||||
submit: Enregistrer le mot de passe
|
||||
back_to_login: Retour à la connexion
|
||||
invitation:
|
||||
@@ -128,11 +128,12 @@ fr:
|
||||
name_label: Nom
|
||||
role_label: "Rôle : %{role}"
|
||||
current_password_label: Mot de passe actuel
|
||||
new_password_label: "Nouveau mot de passe (min. 8 caractères)"
|
||||
new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
|
||||
save_profile: Enregistrer le profil
|
||||
save_password: Mettre à jour le mot de passe
|
||||
common:
|
||||
privacy: Confidentialité
|
||||
support: Support
|
||||
cookies: Cookies
|
||||
terms: Conditions
|
||||
pricing: Tarifs
|
||||
|
||||
@@ -103,7 +103,7 @@ it:
|
||||
password_reset:
|
||||
meta_title: "Nuova password — Match Live TV"
|
||||
title: Scegli una nuova password
|
||||
new_password_label: "Nuova password (min. 8 caratteri)"
|
||||
new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
|
||||
submit: Salva password
|
||||
back_to_login: Torna al login
|
||||
invitation:
|
||||
@@ -128,11 +128,12 @@ it:
|
||||
name_label: Nome
|
||||
role_label: "Ruolo: %{role}"
|
||||
current_password_label: Password attuale
|
||||
new_password_label: "Nuova password (min. 8 caratteri)"
|
||||
new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
|
||||
save_profile: Salva profilo
|
||||
save_password: Aggiorna password
|
||||
common:
|
||||
privacy: Privacy
|
||||
support: Supporto
|
||||
cookies: Cookie
|
||||
terms: Termini
|
||||
pricing: Prezzi
|
||||
|
||||
@@ -154,6 +154,7 @@ Rails.application.routes.draw do
|
||||
get "prezzi", to: "pages#pricing", as: :prezzi
|
||||
get "pricing", to: redirect("/prezzi")
|
||||
get "privacy", to: "pages#privacy", as: :privacy
|
||||
get "support", to: "pages#support", as: :support
|
||||
get "cookie", to: "pages#cookies", as: :cookies
|
||||
get "cookies", to: redirect("/cookie")
|
||||
get "termini", to: "pages#terms", as: :termini
|
||||
@@ -169,6 +170,7 @@ Rails.application.routes.draw do
|
||||
patch "password/reset", to: "password_resets#update"
|
||||
get "account", to: "accounts#show", as: :account
|
||||
patch "account", to: "accounts#update"
|
||||
get "account/password", to: redirect("/account"), as: nil
|
||||
patch "account/password", to: "accounts#update_password", as: :account_password
|
||||
get "clubs/new", to: "clubs#new", as: :new_club
|
||||
post "clubs", to: "clubs#create"
|
||||
|
||||
+4
-4
@@ -1,18 +1,18 @@
|
||||
load Rails.root.join("db/seeds/plans.rb")
|
||||
|
||||
AdminAccount.find_or_create_by!(username: "admin") do |a|
|
||||
a.password = "admin"
|
||||
a.password = "AdminPass123"
|
||||
end
|
||||
|
||||
coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u|
|
||||
u.name = "Coach Demo"
|
||||
u.password = "password123"
|
||||
u.password = "Password123"
|
||||
u.role = "coach"
|
||||
end
|
||||
|
||||
admin = User.find_or_create_by!(email: "admin@matchlivetv.test") do |u|
|
||||
u.name = "Admin"
|
||||
u.password = "password123"
|
||||
u.password = "Password123"
|
||||
u.role = "admin"
|
||||
end
|
||||
|
||||
@@ -41,5 +41,5 @@ match = team.matches.find_or_create_by!(opponent_name: "ASD Eagles Pavia") do |m
|
||||
m.phase = "Semifinale"
|
||||
end
|
||||
|
||||
puts "Seed OK: coach@matchlivetv.test / password123"
|
||||
puts "Seed OK: coach@matchlivetv.test / Password123"
|
||||
puts "Club: #{club.name}, Team: #{team.name}, Match: #{match.opponent_name}"
|
||||
|
||||
+107
-16
@@ -199,6 +199,17 @@ body.nav-menu-open { overflow: hidden; }
|
||||
body.nav-menu-open .site-chrome {
|
||||
z-index: 1300;
|
||||
}
|
||||
/* Keep the close control above the full-screen sheet; hide duplicate mast brand. */
|
||||
body.nav-menu-open .site-masthead {
|
||||
z-index: 1320;
|
||||
background: transparent;
|
||||
border-bottom-color: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
body.nav-menu-open .mast-brand {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.nav-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
@@ -236,16 +247,50 @@ body.nav-menu-open { overflow: hidden; }
|
||||
.nav-panel {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 72px 24px 32px;
|
||||
padding-top: calc(72px + env(safe-area-inset-top, 0px));
|
||||
padding: 16px 24px 32px;
|
||||
padding-top: calc(16px + env(safe-area-inset-top, 0px));
|
||||
padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px));
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.nav-mobile-head {
|
||||
order: -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
margin: 0 0 8px;
|
||||
padding: 0 52px 16px 0; /* room for the close (X) control on the right */
|
||||
border-bottom: 1px solid #252530;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-mobile-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
}
|
||||
.nav-mobile-brand:hover { text-decoration: none; opacity: 0.92; }
|
||||
.nav-mobile-brand .brand { font-size: 1.05rem; }
|
||||
.nav-mobile-brand-logo {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-panel > a,
|
||||
.nav-panel .nav-link-item {
|
||||
display: block;
|
||||
@@ -263,11 +308,13 @@ body.nav-menu-open { overflow: hidden; }
|
||||
}
|
||||
.nav-actions {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
gap: 14px;
|
||||
margin-top: 24px;
|
||||
padding: 24px 0 0;
|
||||
border-top: 1px solid #252530;
|
||||
gap: 0;
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
width: 100%;
|
||||
}
|
||||
.nav-actions .nav-link-item {
|
||||
display: block;
|
||||
@@ -289,10 +336,14 @@ body.nav-menu-open { overflow: hidden; }
|
||||
border-radius: 10px;
|
||||
}
|
||||
.nav-lang {
|
||||
margin-left: 0;
|
||||
margin-top: 4px;
|
||||
margin: 0;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-lang .lang-switcher__menu {
|
||||
/* Keep the list inside the open mobile sheet */
|
||||
z-index: 1320;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,14 +393,22 @@ body.nav-menu-open { overflow: hidden; }
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px 20px;
|
||||
}
|
||||
.nav-mobile-head {
|
||||
display: contents;
|
||||
}
|
||||
.nav-mobile-brand {
|
||||
display: none;
|
||||
}
|
||||
.nav-lang {
|
||||
order: 2;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.nav-actions {
|
||||
order: 1;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.nav-lang {
|
||||
margin-left: 2px;
|
||||
}
|
||||
.nav-backdrop { display: none !important; }
|
||||
}
|
||||
.btn { display: inline-block; padding: 10px 18px; border-radius: 8px; font-weight: 700; font-size: 0.9rem; border: none; cursor: pointer; text-decoration: none; }
|
||||
@@ -1230,7 +1289,22 @@ body.nav-menu-open { overflow: hidden; }
|
||||
.hero-split .hero-cta { flex-direction: column; }
|
||||
.hero-split .hero-cta .btn { width: 100%; text-align: center; }
|
||||
}
|
||||
.section { padding: 40px 0; }
|
||||
.section {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
/* Keep horizontal inset when .section shares a node with .wrap (padding shorthand must not win). */
|
||||
.section.wrap {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.section h2 { font-size: 1.6rem; margin: 0 0 20px; text-align: center; }
|
||||
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
|
||||
.step { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 22px; }
|
||||
@@ -1363,7 +1437,7 @@ body.nav-menu-open { overflow: hidden; }
|
||||
.site-footer__legal { flex: 1 1 100%; margin-top: 4px; }
|
||||
.site-footer__legal p { margin: 0 0 6px; line-height: 1.45; }
|
||||
.site-footer__legal p:last-child { margin-bottom: 0; }
|
||||
.compare-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 0.9rem; }
|
||||
.compare-table { width: 100%; min-width: 520px; border-collapse: collapse; margin-top: 0; font-size: 0.9rem; }
|
||||
.compare-table th, .compare-table td { padding: 10px 12px; border-bottom: 1px solid #2a2a36; text-align: left; }
|
||||
.compare-table th { color: #aaa; font-weight: 600; }
|
||||
.billing-documents h2 { margin-top: 0; }
|
||||
@@ -1372,11 +1446,14 @@ body.nav-menu-open { overflow: hidden; }
|
||||
.card { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
|
||||
|
||||
.seo-prose { max-width: 720px; margin: 0 auto; color: #bbb; line-height: 1.65; }
|
||||
.seo-prose h2 { color: #fff; font-size: 1.25rem; margin: 28px 0 12px; }
|
||||
.seo-prose h2 { color: #fff; font-size: 1.25rem; margin: 28px 0 12px; text-align: left; }
|
||||
.seo-prose h2:first-child { margin-top: 0; }
|
||||
.seo-prose p { margin: 0 0 14px; }
|
||||
.seo-prose ul { margin: 0 0 16px; padding-left: 1.25rem; }
|
||||
.seo-prose a { color: #e53935; }
|
||||
@media (max-width: 899px) {
|
||||
.seo-prose h2 { font-size: 1.15rem; line-height: 1.35; }
|
||||
}
|
||||
.seo-page .seo-lead { color: #aaa; max-width: 640px; line-height: 1.55; margin-bottom: 28px; }
|
||||
.faq-list { max-width: 720px; margin: 0 auto; }
|
||||
.faq-item {
|
||||
@@ -1412,7 +1489,8 @@ body.nav-menu-open { overflow: hidden; }
|
||||
.legal-doc a { color: #e53935; }
|
||||
.legal-meta { color: #888; font-size: 0.88rem; margin-bottom: 24px; }
|
||||
.legal-back { margin-top: 32px; }
|
||||
.legal-table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; font-size: 0.88rem; }
|
||||
.legal-doc .table-scroll { margin: 12px 0 16px; }
|
||||
.legal-table { width: 100%; min-width: 560px; border-collapse: collapse; margin: 0; font-size: 0.88rem; }
|
||||
.legal-table th, .legal-table td { border: 1px solid #2a2a36; padding: 10px 12px; text-align: left; vertical-align: top; }
|
||||
.legal-table th { background: #14141c; color: #ccc; }
|
||||
.legal-accept {
|
||||
@@ -1447,6 +1525,19 @@ body.nav-menu-open { overflow: hidden; }
|
||||
.auth-forgot a { color: #e53935; }
|
||||
table.data { width: 100%; border-collapse: collapse; }
|
||||
table.data th, table.data td { padding: 8px; border-bottom: 1px solid #2a2a36; text-align: left; }
|
||||
/* Wide roster/match tables: scroll inside the card instead of expanding the page. */
|
||||
@media (max-width: 899px) {
|
||||
.card:has(table.data),
|
||||
.team-streaming-staff:has(table.data),
|
||||
.billing-documents:has(table.data) {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
max-width: 100%;
|
||||
}
|
||||
table.data {
|
||||
min-width: 520px;
|
||||
}
|
||||
}
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe PasswordComplexity do
|
||||
describe ".violation" do
|
||||
it "accepts a password with 3 character classes" do
|
||||
expect(described_class.violation("NewPass123")).to be_nil
|
||||
end
|
||||
|
||||
it "accepts symbols instead of one letter class" do
|
||||
expect(described_class.violation("newpass1!")).to be_nil
|
||||
end
|
||||
|
||||
it "rejects passwords that are too short" do
|
||||
expect(described_class.violation("Ab1!")).to eq(:too_short)
|
||||
end
|
||||
|
||||
it "rejects passwords with fewer than 3 classes" do
|
||||
expect(described_class.violation("password123")).to eq(:too_weak)
|
||||
expect(described_class.violation("PASSWORD123")).to eq(:too_weak)
|
||||
expect(described_class.violation("Password")).to eq(:too_weak)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".same_as_current?" do
|
||||
let!(:user) { User.create!(email: "same@example.com", name: "Same", password: "Password123", role: "coach") }
|
||||
|
||||
it "detects when the new password matches the current one" do
|
||||
expect(described_class.same_as_current?(user, "Password123")).to eq(true)
|
||||
expect(described_class.same_as_current?(user, "OtherPass123")).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "User validation" do
|
||||
it "blocks weak passwords on create" do
|
||||
user = User.new(email: "weak@example.com", name: "Weak", password: "password123", role: "coach")
|
||||
expect(user).not_to be_valid
|
||||
expect(user.errors[:password]).to be_present
|
||||
end
|
||||
|
||||
it "allows strong passwords on create" do
|
||||
user = User.new(email: "strong@example.com", name: "Strong", password: "NewPass123", role: "coach")
|
||||
expect(user).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,9 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Account API", type: :request do
|
||||
let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "password123", role: "coach") }
|
||||
let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "Password123", role: "coach") }
|
||||
let(:auth_headers) do
|
||||
post "/api/v1/auth/login", params: { email: user.email, password: "password123" }
|
||||
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
|
||||
token = JSON.parse(response.body).fetch("access_token")
|
||||
{ "Authorization" => "Bearer #{token}" }
|
||||
end
|
||||
@@ -40,29 +40,67 @@ RSpec.describe "Account API", type: :request do
|
||||
it "changes the password with the current password" do
|
||||
patch "/api/v1/account/password",
|
||||
params: {
|
||||
current_password: "password123",
|
||||
password: "newpass123",
|
||||
password_confirmation: "newpass123"
|
||||
current_password: "Password123",
|
||||
password: "NewPass123",
|
||||
password_confirmation: "NewPass123"
|
||||
},
|
||||
headers: auth_headers
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(user.reload.authenticate("newpass123")).to be_truthy
|
||||
expect(user.reload.authenticate("NewPass123")).to be_truthy
|
||||
end
|
||||
|
||||
it "rejects an incorrect current password" do
|
||||
patch "/api/v1/account/password",
|
||||
params: {
|
||||
current_password: "wrong",
|
||||
password: "newpass123",
|
||||
password_confirmation: "newpass123"
|
||||
password: "NewPass123",
|
||||
password_confirmation: "NewPass123"
|
||||
},
|
||||
headers: auth_headers
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(user.reload.authenticate("password123")).to be_truthy
|
||||
expect(user.reload.authenticate("Password123")).to be_truthy
|
||||
end
|
||||
|
||||
it "rejects a password that fails complexity rules" do
|
||||
patch "/api/v1/account/password",
|
||||
params: {
|
||||
current_password: "Password123",
|
||||
password: "newpass123",
|
||||
password_confirmation: "newpass123"
|
||||
},
|
||||
headers: auth_headers.merge("Accept-Language" => "en")
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)["error"]).to match(/3 of/i)
|
||||
expect(user.reload.authenticate("Password123")).to be_truthy
|
||||
end
|
||||
|
||||
it "rejects reusing the current password" do
|
||||
patch "/api/v1/account/password",
|
||||
params: {
|
||||
current_password: "Password123",
|
||||
password: "Password123",
|
||||
password_confirmation: "Password123"
|
||||
},
|
||||
headers: auth_headers.merge("Accept-Language" => "en")
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)["error"]).to match(/different/i)
|
||||
expect(user.reload.authenticate("Password123")).to be_truthy
|
||||
end
|
||||
|
||||
it "localizes password errors from Accept-Language" do
|
||||
patch "/api/v1/account/password",
|
||||
params: {
|
||||
current_password: "Password123",
|
||||
password: "Password123",
|
||||
password_confirmation: "Password123"
|
||||
},
|
||||
headers: auth_headers.merge("Accept-Language" => "it")
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)["error"]).to eq("La nuova password deve essere diversa da quella attuale")
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v1/auth/password/forgot" do
|
||||
describe "POST /api/v1/auth/password/forgot" do
|
||||
it "always returns ok and sends mail when the user exists" do
|
||||
expect {
|
||||
post "/api/v1/auth/password/forgot", params: { email: user.email }
|
||||
@@ -77,5 +115,13 @@ RSpec.describe "Account API", type: :request do
|
||||
}.not_to change { ActionMailer::Base.deliveries.size }
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "localizes the response message from Accept-Language" do
|
||||
post "/api/v1/auth/password/forgot",
|
||||
params: { email: user.email },
|
||||
headers: { "Accept-Language" => "fr" }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(JSON.parse(response.body)["message"]).to include("réinitialiser")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Auth API", type: :request do
|
||||
let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "password123", role: "coach") }
|
||||
let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "Password123", role: "coach") }
|
||||
|
||||
it "logs in with valid credentials" do
|
||||
post "/api/v1/auth/login", params: { email: user.email, password: "password123" }
|
||||
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(JSON.parse(response.body)).to have_key("access_token")
|
||||
end
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Public support page", type: :request do
|
||||
it "è pubblica, indicizzabile e mostra l'email di supporto configurata" do
|
||||
get public_support_path
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include(MatchLiveTv.support_email)
|
||||
expect(response.body).to include("mailto:#{MatchLiveTv.support_email}")
|
||||
expect(response.body).to include(I18n.t("legal.support.h1", locale: :it))
|
||||
expect(response.body).to include('rel="canonical"')
|
||||
expect(response.body).to include(public_support_path)
|
||||
expect(response.body).to include(public_privacy_path)
|
||||
end
|
||||
|
||||
it "non espone CTA o link commerciali (App Store Review)" do
|
||||
get public_support_path
|
||||
body = response.body
|
||||
|
||||
expect(body).not_to include(public_prezzi_path)
|
||||
expect(body).not_to include(public_signup_path)
|
||||
expect(body).not_to include('href="/prezzi"')
|
||||
expect(body).not_to include('href="/signup"')
|
||||
expect(body).not_to include(I18n.t("nav.signup", locale: :it))
|
||||
expect(body).not_to include(I18n.t("nav.pricing", locale: :it))
|
||||
expect(body).not_to include(I18n.t("common.pricing", locale: :it))
|
||||
expect(body).not_to match(/\bPremium Light\b/i)
|
||||
expect(body).not_to match(/\bPremium Full\b/i)
|
||||
expect(body).not_to match(/\bpiano Free\b/i)
|
||||
expect(body).not_to match(/\bAbbonati\b/i)
|
||||
expect(body).not_to match(/\bAcquista\b/i)
|
||||
end
|
||||
|
||||
{
|
||||
"it" => "Supporto Match Live TV",
|
||||
"en" => "Match Live TV Support",
|
||||
"fr" => "Support Match Live TV",
|
||||
"de" => "Match Live TV Support",
|
||||
"es" => "Soporte Match Live TV"
|
||||
}.each do |locale, heading|
|
||||
it "renderizza correttamente in #{locale} senza translation missing" do
|
||||
cookies[:mltv_locale] = locale
|
||||
get public_support_path
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include(heading)
|
||||
expect(response.body).not_to include("translation missing")
|
||||
end
|
||||
end
|
||||
|
||||
it "include /support nella sitemap" do
|
||||
get "/sitemap.xml"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include("#{MatchLiveTv.app_public_url.chomp('/')}/support")
|
||||
end
|
||||
end
|
||||
@@ -38,10 +38,10 @@ Solo se la società vuole il **proprio** canale invece di Match Live TV:
|
||||
|
||||
Nella home app (**Partite**):
|
||||
|
||||
- **Partita programmata** — scegli dal calendario una gara già inserita (sito o app)
|
||||
- **Nuova partita** — programma data/ora oppure **Avvia subito** senza orario
|
||||
- Oppure tocca una gara già in calendario (sito o app)
|
||||
|
||||
Poi tocca la card o conferma dal foglio: si apre il wizard (Partita → Trasmissione → …).
|
||||
Poi conferma dal foglio o tocca la card: si apre il wizard (Partita → Trasmissione → …).
|
||||
|
||||
## 4. Avvia diretta
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Gap Android → iOS: allineamento app nativa
|
||||
|
||||
Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android `2.0.5-native`**.
|
||||
Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android**.
|
||||
|
||||
**Aggiornato:** 24 luglio 2026
|
||||
**Riferimento Android (produzione / telefono / Play):** `2.0.5-native` (`versionCode` **26**), API `https://www.matchlivetv.it`
|
||||
**Stato iOS attuale:** marketing `2.0.5`, build `26` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast)
|
||||
**Aggiornato:** 8 agosto 2026
|
||||
**Riferimento Android (produzione / telefono / Play):** `2.0.10-native` (`versionCode` **31**), API `https://www.matchlivetv.it`
|
||||
**Stato iOS attuale:** marketing `2.0.10`, build `31` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast, account/forgot)
|
||||
|
||||
Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast) + bump versione, **senza** i bug di sessione/crash già risolti su Android 2.0.5.
|
||||
Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast, account) + bump versione, **senza** i bug di sessione/crash già risolti su Android.
|
||||
|
||||
---
|
||||
|
||||
@@ -59,7 +59,7 @@ Queste sono regressioni/bug già visti su Android; **non ripeterli** su iOS.
|
||||
| Broadcast + score dialog | Sì | `broadcast.*` / `score.*` | **Fatto** |
|
||||
| Catalogo stringhe | `values*` (~230) | `AppLanguage.swift` L10n (~225, no FGS) | **Fatto** |
|
||||
| Logout UI | Icona | Icona SF Symbol | **Fatto** |
|
||||
| Versione | `2.0.5-native` / **26** | `2.0.5` / **26** | **Fatto** |
|
||||
| Versione | `2.0.10-native` / **31** | `2.0.10` / **31** | **Fatto** |
|
||||
| RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK |
|
||||
|
||||
### Residui fuori scope Android (opzionali)
|
||||
@@ -141,7 +141,7 @@ xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \
|
||||
|
||||
Nessuna modifica server richiesta.
|
||||
|
||||
Versione store target: **`2.0.5` / build `26`** (parità con Android `2.0.5-native` / versionCode `26`).
|
||||
Versione store target: **`2.0.10` / build `31`** (parità con Android `2.0.10-native` / versionCode `31`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# iOS — allineamento password policy + errori API localizzati
|
||||
|
||||
Documento operativo per **Cursor su Mac**: allineare l’app iOS a backend/Android dopo il ramo `feature/password-policy` (merge su `main`).
|
||||
|
||||
**Aggiornato:** 2026-08-08
|
||||
**Android di riferimento:** `2.0.10-native` (`versionCode` **31**)
|
||||
**iOS (allineato):** marketing `2.0.10` / build `31`
|
||||
**API produzione:** `https://www.matchlivetv.it`
|
||||
|
||||
---
|
||||
|
||||
## Contesto (già in produzione backend dopo questo rilascio)
|
||||
|
||||
### Policy password (server)
|
||||
|
||||
Definita in `backend/app/models/concerns/password_complexity.rb`:
|
||||
|
||||
- minimo **8** caratteri, massimo **72** byte (bcrypt)
|
||||
- almeno **3 classi su 4**: minuscole, maiuscole, numeri, simboli
|
||||
- in **cambio password** e **reset**: la nuova password **non** può coincidere con quella attuale
|
||||
|
||||
Validazione ActiveModel su `User` / `AdminAccount`. Endpoint che la applicano:
|
||||
|
||||
| Endpoint | Note |
|
||||
|----------|------|
|
||||
| `PATCH /api/v1/account/password` | App native + stessi codici errore |
|
||||
| `PATCH /account/password` (web) | Flash I18n |
|
||||
| reset password pubblico | Stesse regole |
|
||||
| registrazione (`auth/register`) | Validazione modello |
|
||||
|
||||
### Errori API localizzati
|
||||
|
||||
`ApplicationController#set_api_locale` legge, in ordine:
|
||||
|
||||
1. header `X-Locale`
|
||||
2. `Accept-Language`
|
||||
3. `I18n.default_locale` (fallback; **retrocompatibile** se l’app vecchia non manda nulla)
|
||||
|
||||
Risposta invariata: `{ "error": "<messaggio già tradotto>" }` (o `{ "message": "..." }` su alcuni successi).
|
||||
|
||||
Chiavi rilevanti (`backend/config/locales/app.{it,en,fr,de,es}.yml`):
|
||||
|
||||
- `flash.accounts.password_too_short`
|
||||
- `flash.accounts.password_too_long`
|
||||
- `flash.accounts.password_too_weak`
|
||||
- `flash.accounts.password_same_as_current`
|
||||
- `flash.accounts.password_current_incorrect`
|
||||
- `flash.accounts.password_mismatch`
|
||||
- `flash.accounts.password_updated` / `name_required`
|
||||
- analoghi in `flash.password_resets.*` e `flash.sessions.*`
|
||||
|
||||
---
|
||||
|
||||
## Stato iOS vs Android
|
||||
|
||||
| Area | Android 2.0.10 | iOS | Stato |
|
||||
|------|----------------|-----|-------|
|
||||
| Header `Accept-Language` su tutte le request API | OkHttp interceptor | `MatchLiveAPI.request` + `multipartPatch` | **OK** |
|
||||
| Hint UI nuova password | `account_new_password` | `account.new.password` | **OK** |
|
||||
| Schermata Account | AccountScreen | AccountScreen | **OK** |
|
||||
| Forgot password | ForgotPasswordScreen | ForgotPasswordScreen | **OK** |
|
||||
| Parsing errori API | body `error` | `APIError.friendlyHttpMessage` | **OK** |
|
||||
| Client-side pre-check complessità | No | No | N/A (come Android) |
|
||||
| Versione store | `2.0.10-native` / **31** | `2.0.10` / **31** | **OK** |
|
||||
| Signup in-app | Non principale | Nessuna UI register | N/A (signup web) |
|
||||
|
||||
### File iOS
|
||||
|
||||
```
|
||||
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift
|
||||
native/ios/MatchLiveTv/Core/AppLanguage.swift
|
||||
native/ios/MatchLiveTv/UI/Account/AccountScreen.swift
|
||||
native/ios/MatchLiveTv/UI/Login/ForgotPasswordScreen.swift
|
||||
native/ios/MatchLiveTv/Resources/Info.plist
|
||||
native/ios/generate_xcodeproj.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterio “fatto”
|
||||
|
||||
- [x] `Accept-Language` su login, me, account, change password, forgot (`MatchLiveAPI`)
|
||||
- [x] Hint campo nuova password parla di “min. 8 / 3 tipi”
|
||||
- [x] Catalogo stringhe account/forgot in it/en/fr/de/es
|
||||
- [x] Versione bumpata a `2.0.10` / `31`
|
||||
- [ ] QA manuale errori password in lingua UI (weak / same / mismatch / success)
|
||||
|
||||
Quando rilasci su TestFlight/App Store, conferma la build `31`.
|
||||
@@ -15,6 +15,8 @@ YOUTUBE_MOCK_STREAM_KEY=mock-stream-key
|
||||
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
||||
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
||||
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
|
||||
# Opzionale: email dedicata di supporto (default = info@matchlivetv.it)
|
||||
# SUPPORT_CONTACT_EMAIL=info@matchlivetv.it
|
||||
MAILER_FROM=Match Live TV <noreply@matchlivetv.it>
|
||||
PASSWORD_RESET_EXPIRY_HOURS=2
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ RAILS_LOG_LEVEL=info
|
||||
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
||||
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
||||
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
|
||||
# Opzionale: email di supporto App Store (default = info@matchlivetv.it)
|
||||
# SUPPORT_CONTACT_EMAIL=info@matchlivetv.it
|
||||
PRIVACY_CONTROLLER_VAT=
|
||||
|
||||
# Email transazionali (reset password, inviti)
|
||||
|
||||
@@ -73,6 +73,7 @@ services:
|
||||
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
||||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
||||
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
|
||||
SUPPORT_CONTACT_EMAIL: ${SUPPORT_CONTACT_EMAIL:-}
|
||||
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
||||
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
||||
PRIVACY_CONTROLLER_VAT: ${PRIVACY_CONTROLLER_VAT:-}
|
||||
|
||||
@@ -71,6 +71,7 @@ services:
|
||||
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
||||
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
||||
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
|
||||
SUPPORT_CONTACT_EMAIL: ${SUPPORT_CONTACT_EMAIL:-}
|
||||
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlivetv.it>}
|
||||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*.iml
|
||||
/captures/
|
||||
/app/build/
|
||||
/dist/
|
||||
.DS_Store
|
||||
keystore.properties
|
||||
keystore/
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.matchlivetv.match_live_tv.data
|
||||
|
||||
import android.content.Context
|
||||
import com.matchlivetv.match_live_tv.core.AppConfig
|
||||
import com.matchlivetv.match_live_tv.core.AppLocale
|
||||
import com.matchlivetv.match_live_tv.core.TokenStore
|
||||
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
|
||||
import com.matchlivetv.match_live_tv.data.cable.SessionCableService
|
||||
@@ -37,10 +38,20 @@ class AppContainer(context: Context) {
|
||||
accessToken?.let { header("Authorization", "Bearer $it") }
|
||||
header("Accept", "application/json")
|
||||
header("Content-Type", "application/json")
|
||||
header("Accept-Language", apiLanguageTag())
|
||||
}.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun apiLanguageTag(): String {
|
||||
val tag = AppLocale.currentTag(appContext)
|
||||
return if (tag.isBlank()) {
|
||||
java.util.Locale.getDefault().language.ifBlank { "it" }
|
||||
} else {
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
private val okHttp = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
|
||||
+5
-13
@@ -57,7 +57,6 @@ import com.matchlivetv.match_live_tv.ui.components.LanguagePickerDialog
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchLiveWordmark
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -237,18 +236,11 @@ fun MatchesScreen(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
MatchSecondaryButton(
|
||||
label = stringResource(R.string.matches_schedule).uppercase(),
|
||||
onClick = { showScheduleSheet = true },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
MatchPrimaryButton(
|
||||
label = stringResource(R.string.matches_new).uppercase(),
|
||||
onClick = { showNewMatchSheet = true },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
MatchPrimaryButton(
|
||||
label = stringResource(R.string.matches_new).uppercase(),
|
||||
onClick = { showNewMatchSheet = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
activeTeam?.let { team ->
|
||||
TeamPickerBar(
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
<string name="login_error_generic">Anmeldung fehlgeschlagen</string>
|
||||
<string name="matches_title">Spiele</string>
|
||||
<string name="matches_hello">Hallo, %1$s</string>
|
||||
<string name="matches_subtitle">Nimm einen laufenden Livestream wieder auf oder starte ein geplantes Spiel.</string>
|
||||
<string name="matches_schedule">Geplantes Spiel</string>
|
||||
<string name="matches_subtitle">Nimm einen laufenden Livestream wieder auf oder starte ein neues Spiel.</string>
|
||||
<string name="matches_new">Neues Spiel</string>
|
||||
<string name="matches_empty_title">Keine Spiele im Kalender</string>
|
||||
<string name="matches_scheduled_title">Geplante Spiele</string>
|
||||
<string name="matches_ready_title">Bereit zum Start</string>
|
||||
<string name="matches_empty_hint">Plane ein Spiel oder starte ein neues mit «Neues Spiel».</string>
|
||||
<string name="matches_empty_hint">Tippe auf «Neues Spiel», um im Voraus zu planen oder sofort zu starten.</string>
|
||||
<string name="matches_active_team">Aktives Team: %1$s.</string>
|
||||
<string name="matches_multi_team_hint">Du hast mehrere Teams: prüfe die oben ausgewählte.</string>
|
||||
<string name="matches_no_other">Keine weiteren Spiele im Kalender.</string>
|
||||
@@ -252,7 +251,7 @@ Dies kann nicht rückgängig gemacht werden.</string>
|
||||
<string name="account_name_label">Name</string>
|
||||
<string name="account_role">Rolle: %1$s</string>
|
||||
<string name="account_current_password">Aktuelles Passwort</string>
|
||||
<string name="account_new_password">Neues Passwort (mind. 8 Zeichen)</string>
|
||||
<string name="account_new_password">Neues Passwort (mind. 8, mindestens 3 Zeichenarten)</string>
|
||||
<string name="account_confirm_password">Passwort bestätigen</string>
|
||||
<string name="account_save_profile">Profil speichern</string>
|
||||
<string name="account_save_password">Passwort aktualisieren</string>
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
<string name="login_error_generic">Login failed</string>
|
||||
<string name="matches_title">Matches</string>
|
||||
<string name="matches_hello">Hi, %1$s</string>
|
||||
<string name="matches_subtitle">Resume a live stream or start a scheduled match.</string>
|
||||
<string name="matches_schedule">Scheduled match</string>
|
||||
<string name="matches_subtitle">Resume a live stream or start a new match.</string>
|
||||
<string name="matches_new">New match</string>
|
||||
<string name="matches_empty_title">No matches on the calendar</string>
|
||||
<string name="matches_scheduled_title">Scheduled matches</string>
|
||||
<string name="matches_ready_title">Ready to start</string>
|
||||
<string name="matches_empty_hint">Schedule a match or start a new one with «New match».</string>
|
||||
<string name="matches_empty_hint">Tap «New match» to schedule ahead or start right away.</string>
|
||||
<string name="matches_active_team">Active team: %1$s.</string>
|
||||
<string name="matches_multi_team_hint">You have multiple teams: check the one selected above.</string>
|
||||
<string name="matches_no_other">No other matches on the calendar.</string>
|
||||
@@ -252,7 +251,7 @@ This cannot be undone.</string>
|
||||
<string name="account_name_label">Name</string>
|
||||
<string name="account_role">Role: %1$s</string>
|
||||
<string name="account_current_password">Current password</string>
|
||||
<string name="account_new_password">New password (min. 8 characters)</string>
|
||||
<string name="account_new_password">New password (min. 8, at least 3 character types)</string>
|
||||
<string name="account_confirm_password">Confirm password</string>
|
||||
<string name="account_save_profile">Save profile</string>
|
||||
<string name="account_save_password">Update password</string>
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
<string name="login_error_generic">Error de acceso</string>
|
||||
<string name="matches_title">Partidos</string>
|
||||
<string name="matches_hello">Hola, %1$s</string>
|
||||
<string name="matches_subtitle">Reanuda un directo en curso o inicia un partido programado.</string>
|
||||
<string name="matches_schedule">Partido programado</string>
|
||||
<string name="matches_subtitle">Reanuda un directo en curso o inicia un nuevo partido.</string>
|
||||
<string name="matches_new">Nuevo partido</string>
|
||||
<string name="matches_empty_title">Ningún partido en el calendario</string>
|
||||
<string name="matches_scheduled_title">Partidos programados</string>
|
||||
<string name="matches_ready_title">Listos para empezar</string>
|
||||
<string name="matches_empty_hint">Programa un partido o inicia uno nuevo con «Nuevo partido».</string>
|
||||
<string name="matches_empty_hint">Toca «Nuevo partido» para programar con antelación o empezar ya.</string>
|
||||
<string name="matches_active_team">Equipo activo: %1$s.</string>
|
||||
<string name="matches_multi_team_hint">Tienes varios equipos: comprueba el seleccionado arriba.</string>
|
||||
<string name="matches_no_other">Ningún otro partido en el calendario.</string>
|
||||
@@ -252,7 +251,7 @@ Esta acción no se puede deshacer.</string>
|
||||
<string name="account_name_label">Nombre</string>
|
||||
<string name="account_role">Rol: %1$s</string>
|
||||
<string name="account_current_password">Contraseña actual</string>
|
||||
<string name="account_new_password">Nueva contraseña (mín. 8 caracteres)</string>
|
||||
<string name="account_new_password">Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)</string>
|
||||
<string name="account_confirm_password">Confirmar contraseña</string>
|
||||
<string name="account_save_profile">Guardar perfil</string>
|
||||
<string name="account_save_password">Actualizar contraseña</string>
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
<string name="login_error_generic">Échec de la connexion</string>
|
||||
<string name="matches_title">Matchs</string>
|
||||
<string name="matches_hello">Bonjour, %1$s</string>
|
||||
<string name="matches_subtitle">Reprenez un direct en cours ou démarrez un match programmé.</string>
|
||||
<string name="matches_schedule">Match programmé</string>
|
||||
<string name="matches_subtitle">Reprenez un direct en cours ou démarrez un nouveau match.</string>
|
||||
<string name="matches_new">Nouveau match</string>
|
||||
<string name="matches_empty_title">Aucun match au calendrier</string>
|
||||
<string name="matches_scheduled_title">Matchs programmés</string>
|
||||
<string name="matches_ready_title">Prêts à démarrer</string>
|
||||
<string name="matches_empty_hint">Programmez un match ou démarrez-en un avec « Nouveau match ».</string>
|
||||
<string name="matches_empty_hint">Touchez « Nouveau match » pour planifier à l\'avance ou démarrer tout de suite.</string>
|
||||
<string name="matches_active_team">Équipe active : %1$s.</string>
|
||||
<string name="matches_multi_team_hint">Vous avez plusieurs équipes : vérifiez celle sélectionnée ci-dessus.</string>
|
||||
<string name="matches_no_other">Aucun autre match au calendrier.</string>
|
||||
@@ -252,7 +251,7 @@ Cette action est irréversible.</string>
|
||||
<string name="account_name_label">Nom</string>
|
||||
<string name="account_role">Rôle : %1$s</string>
|
||||
<string name="account_current_password">Mot de passe actuel</string>
|
||||
<string name="account_new_password">Nouveau mot de passe (min. 8 caractères)</string>
|
||||
<string name="account_new_password">Nouveau mot de passe (min. 8, au moins 3 types de caractères)</string>
|
||||
<string name="account_confirm_password">Confirmer le mot de passe</string>
|
||||
<string name="account_save_profile">Enregistrer le profil</string>
|
||||
<string name="account_save_password">Mettre à jour le mot de passe</string>
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
<string name="login_error_generic">Login fallito</string>
|
||||
<string name="matches_title">Partite</string>
|
||||
<string name="matches_hello">Ciao, %1$s</string>
|
||||
<string name="matches_subtitle">Riprendi una diretta in corso o avvia una partita programmata.</string>
|
||||
<string name="matches_schedule">Partita programmata</string>
|
||||
<string name="matches_subtitle">Riprendi una diretta in corso o avvia una nuova partita.</string>
|
||||
<string name="matches_new">Nuova partita</string>
|
||||
<string name="matches_empty_title">Nessuna partita in calendario</string>
|
||||
<string name="matches_scheduled_title">Partite programmate</string>
|
||||
<string name="matches_ready_title">Pronte da avviare</string>
|
||||
<string name="matches_empty_hint">Programma una partita o avviane una nuova con «Nuova partita».</string>
|
||||
<string name="matches_empty_hint">Tocca «Nuova partita» per programmare in anticipo o avviare subito.</string>
|
||||
<string name="matches_active_team">Squadra attiva: %1$s.</string>
|
||||
<string name="matches_multi_team_hint">Hai più squadre: verifica quella selezionata sopra.</string>
|
||||
<string name="matches_no_other">Nessuna altra partita in calendario.</string>
|
||||
@@ -252,7 +251,7 @@ L\'operazione non si può annullare.</string>
|
||||
<string name="account_name_label">Nome</string>
|
||||
<string name="account_role">Ruolo: %1$s</string>
|
||||
<string name="account_current_password">Password attuale</string>
|
||||
<string name="account_new_password">Nuova password (min. 8 caratteri)</string>
|
||||
<string name="account_new_password">Nuova password (min. 8, almeno 3 tipi di caratteri)</string>
|
||||
<string name="account_confirm_password">Conferma password</string>
|
||||
<string name="account_save_profile">Salva profilo</string>
|
||||
<string name="account_save_password">Aggiorna password</string>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme LastUpgradeVersion="1600" version="1.7">
|
||||
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2660"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
|
||||
BuildableName = "MatchLiveTv.app"
|
||||
BlueprintName = "MatchLiveTv"
|
||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F6079500E62048DB89AE3866" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "20C695DBFB4042879308B0F5"
|
||||
BuildableName = "MatchLiveTvTests.xctest"
|
||||
BlueprintName = "MatchLiveTvTests"
|
||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference skipped="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F6079500E62048DB89AE3866" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "20C695DBFB4042879308B0F5"
|
||||
BuildableName = "MatchLiveTvTests.xctest"
|
||||
BlueprintName = "MatchLiveTvTests"
|
||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES">
|
||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
|
||||
BuildableName = "MatchLiveTv.app"
|
||||
BlueprintName = "MatchLiveTv"
|
||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
|
||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
|
||||
BuildableName = "MatchLiveTv.app"
|
||||
BlueprintName = "MatchLiveTv"
|
||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction buildConfiguration="Debug"/>
|
||||
<ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
||||
@@ -81,7 +81,7 @@ enum L10n {
|
||||
"account.confirm.password": "Conferma password",
|
||||
"account.current.password": "Password attuale",
|
||||
"account.name.label": "Nome",
|
||||
"account.new.password": "Nuova password (min. 8 caratteri)",
|
||||
"account.new.password": "Nuova password (min. 8, almeno 3 tipi di caratteri)",
|
||||
"account.password.heading": "Cambia password",
|
||||
"account.password.saved": "Password aggiornata",
|
||||
"account.profile.heading": "Profilo",
|
||||
@@ -194,7 +194,7 @@ enum L10n {
|
||||
"match.status.start": "AVVIA",
|
||||
"matches.active.team": "Squadra attiva: %1$@.",
|
||||
"matches.change": "Cambia",
|
||||
"matches.empty.hint": "Programma una partita o avviane una nuova con «Nuova partita».",
|
||||
"matches.empty.hint": "Tocca «Nuova partita» per programmare in anticipo o avviare subito.",
|
||||
"matches.empty.title": "Nessuna partita in calendario",
|
||||
"matches.hello": "Ciao, %1$@",
|
||||
"matches.load.error": "Errore caricamento",
|
||||
@@ -212,9 +212,8 @@ enum L10n {
|
||||
"matches.no.team.title": "Nessuna squadra assegnata",
|
||||
"matches.ready.title": "Pronte da avviare",
|
||||
"matches.retry": "Riprova",
|
||||
"matches.schedule": "Partita programmata",
|
||||
"matches.scheduled.title": "Partite programmate",
|
||||
"matches.subtitle": "Riprendi una diretta in corso o avvia una partita programmata.",
|
||||
"matches.subtitle": "Riprendi una diretta in corso o avvia una nuova partita.",
|
||||
"matches.tap.change.team": "Tocca per cambiare squadra",
|
||||
"matches.team.for.live": "Squadra per la diretta",
|
||||
"matches.title": "Partite",
|
||||
@@ -372,7 +371,7 @@ enum L10n {
|
||||
"account.confirm.password": "Confirm password",
|
||||
"account.current.password": "Current password",
|
||||
"account.name.label": "Name",
|
||||
"account.new.password": "New password (min. 8 characters)",
|
||||
"account.new.password": "New password (min. 8, at least 3 character types)",
|
||||
"account.password.heading": "Change password",
|
||||
"account.password.saved": "Password updated",
|
||||
"account.profile.heading": "Profile",
|
||||
@@ -381,7 +380,7 @@ enum L10n {
|
||||
"account.save.password": "Update password",
|
||||
"account.save.profile": "Save profile",
|
||||
"account.title": "Account",
|
||||
"forgot.password.lead": "Enter your account email: we'll send you a link to reset your password.",
|
||||
"forgot.password.lead": "Enter your account email: we will send you a link to reset your password.",
|
||||
"forgot.password.submit": "Send reset link",
|
||||
"forgot.password.success": "If the email is registered, you will receive a password reset link shortly.",
|
||||
"forgot.password.title": "Forgot password",
|
||||
@@ -485,7 +484,7 @@ enum L10n {
|
||||
"match.status.start": "START",
|
||||
"matches.active.team": "Active team: %1$@.",
|
||||
"matches.change": "Change",
|
||||
"matches.empty.hint": "Schedule a match or start a new one with «New match».",
|
||||
"matches.empty.hint": "Tap «New match» to schedule ahead or start right away.",
|
||||
"matches.empty.title": "No matches on the calendar",
|
||||
"matches.hello": "Hi, %1$@",
|
||||
"matches.load.error": "Loading error",
|
||||
@@ -503,9 +502,8 @@ enum L10n {
|
||||
"matches.no.team.title": "No team assigned",
|
||||
"matches.ready.title": "Ready to start",
|
||||
"matches.retry": "Retry",
|
||||
"matches.schedule": "Scheduled match",
|
||||
"matches.scheduled.title": "Scheduled matches",
|
||||
"matches.subtitle": "Resume a live stream or start a scheduled match.",
|
||||
"matches.subtitle": "Resume a live stream or start a new match.",
|
||||
"matches.tap.change.team": "Tap to change team",
|
||||
"matches.team.for.live": "Team for the live stream",
|
||||
"matches.title": "Matches",
|
||||
@@ -663,7 +661,7 @@ enum L10n {
|
||||
"account.confirm.password": "Confirmer le mot de passe",
|
||||
"account.current.password": "Mot de passe actuel",
|
||||
"account.name.label": "Nom",
|
||||
"account.new.password": "Nouveau mot de passe (min. 8 caractères)",
|
||||
"account.new.password": "Nouveau mot de passe (min. 8, au moins 3 types de caractères)",
|
||||
"account.password.heading": "Changer le mot de passe",
|
||||
"account.password.saved": "Mot de passe mis à jour",
|
||||
"account.profile.heading": "Profil",
|
||||
@@ -776,7 +774,7 @@ enum L10n {
|
||||
"match.status.start": "DÉMARRER",
|
||||
"matches.active.team": "Équipe active : %1$@.",
|
||||
"matches.change": "Changer",
|
||||
"matches.empty.hint": "Programmez un match ou démarrez-en un avec « Nouveau match ».",
|
||||
"matches.empty.hint": "Touchez « Nouveau match » pour planifier à l'avance ou démarrer tout de suite.",
|
||||
"matches.empty.title": "Aucun match au calendrier",
|
||||
"matches.hello": "Bonjour, %1$@",
|
||||
"matches.load.error": "Erreur de chargement",
|
||||
@@ -794,9 +792,8 @@ enum L10n {
|
||||
"matches.no.team.title": "Aucune équipe assignée",
|
||||
"matches.ready.title": "Prêts à démarrer",
|
||||
"matches.retry": "Réessayer",
|
||||
"matches.schedule": "Match programmé",
|
||||
"matches.scheduled.title": "Matchs programmés",
|
||||
"matches.subtitle": "Reprenez un direct en cours ou démarrez un match programmé.",
|
||||
"matches.subtitle": "Reprenez un direct en cours ou démarrez un nouveau match.",
|
||||
"matches.tap.change.team": "Appuyez pour changer d'équipe",
|
||||
"matches.team.for.live": "Équipe pour le direct",
|
||||
"matches.title": "Matchs",
|
||||
@@ -954,7 +951,7 @@ enum L10n {
|
||||
"account.confirm.password": "Passwort bestätigen",
|
||||
"account.current.password": "Aktuelles Passwort",
|
||||
"account.name.label": "Name",
|
||||
"account.new.password": "Neues Passwort (mind. 8 Zeichen)",
|
||||
"account.new.password": "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)",
|
||||
"account.password.heading": "Passwort ändern",
|
||||
"account.password.saved": "Passwort aktualisiert",
|
||||
"account.profile.heading": "Profil",
|
||||
@@ -1067,7 +1064,7 @@ enum L10n {
|
||||
"match.status.start": "STARTEN",
|
||||
"matches.active.team": "Aktives Team: %1$@.",
|
||||
"matches.change": "Wechseln",
|
||||
"matches.empty.hint": "Plane ein Spiel oder starte ein neues mit «Neues Spiel».",
|
||||
"matches.empty.hint": "Tippe auf «Neues Spiel», um im Voraus zu planen oder sofort zu starten.",
|
||||
"matches.empty.title": "Keine Spiele im Kalender",
|
||||
"matches.hello": "Hallo, %1$@",
|
||||
"matches.load.error": "Ladefehler",
|
||||
@@ -1085,9 +1082,8 @@ enum L10n {
|
||||
"matches.no.team.title": "Kein Team zugewiesen",
|
||||
"matches.ready.title": "Bereit zum Start",
|
||||
"matches.retry": "Erneut versuchen",
|
||||
"matches.schedule": "Geplantes Spiel",
|
||||
"matches.scheduled.title": "Geplante Spiele",
|
||||
"matches.subtitle": "Nimm einen laufenden Livestream wieder auf oder starte ein geplantes Spiel.",
|
||||
"matches.subtitle": "Nimm einen laufenden Livestream wieder auf oder starte ein neues Spiel.",
|
||||
"matches.tap.change.team": "Tippen zum Teamwechsel",
|
||||
"matches.team.for.live": "Team für den Livestream",
|
||||
"matches.title": "Spiele",
|
||||
@@ -1245,7 +1241,7 @@ enum L10n {
|
||||
"account.confirm.password": "Confirmar contraseña",
|
||||
"account.current.password": "Contraseña actual",
|
||||
"account.name.label": "Nombre",
|
||||
"account.new.password": "Nueva contraseña (mín. 8 caracteres)",
|
||||
"account.new.password": "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)",
|
||||
"account.password.heading": "Cambiar contraseña",
|
||||
"account.password.saved": "Contraseña actualizada",
|
||||
"account.profile.heading": "Perfil",
|
||||
@@ -1358,7 +1354,7 @@ enum L10n {
|
||||
"match.status.start": "INICIAR",
|
||||
"matches.active.team": "Equipo activo: %1$@.",
|
||||
"matches.change": "Cambiar",
|
||||
"matches.empty.hint": "Programa un partido o inicia uno nuevo con «Nuevo partido».",
|
||||
"matches.empty.hint": "Toca «Nuevo partido» para programar con antelación o empezar ya.",
|
||||
"matches.empty.title": "Ningún partido en el calendario",
|
||||
"matches.hello": "Hola, %1$@",
|
||||
"matches.load.error": "Error de carga",
|
||||
@@ -1376,9 +1372,8 @@ enum L10n {
|
||||
"matches.no.team.title": "Ningún equipo asignado",
|
||||
"matches.ready.title": "Listos para empezar",
|
||||
"matches.retry": "Reintentar",
|
||||
"matches.schedule": "Partido programado",
|
||||
"matches.scheduled.title": "Partidos programados",
|
||||
"matches.subtitle": "Reanuda un directo en curso o inicia un partido programado.",
|
||||
"matches.subtitle": "Reanuda un directo en curso o inicia un nuevo partido.",
|
||||
"matches.tap.change.team": "Toca para cambiar de equipo",
|
||||
"matches.team.for.live": "Equipo para el directo",
|
||||
"matches.title": "Partidos",
|
||||
|
||||
@@ -240,6 +240,7 @@ final class MatchLiveAPI: @unchecked Sendable {
|
||||
request.httpMethod = "PATCH"
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
|
||||
if let accessToken { request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") }
|
||||
request.httpBody = MultipartBuilder.build(fields: fields, boundary: boundary)
|
||||
return try await execute(request)
|
||||
@@ -253,6 +254,7 @@ final class MatchLiveAPI: @unchecked Sendable {
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(path))
|
||||
request.httpMethod = method
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
|
||||
if body != nil {
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = try ApiInstant.encoder.encode(body)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 216 KiB |
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0.5</string>
|
||||
<string>2.0.10</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -32,7 +32,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>26</string>
|
||||
<string>32</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
@@ -55,6 +55,14 @@
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
|
||||
@@ -17,6 +17,7 @@ struct MatchPrimaryButton: View {
|
||||
.font(MatchTypography.labelLarge)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
.padding(.horizontal, 14)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -21,5 +21,8 @@ struct MatchScreenScaffold<Content: View, TopBar: View>: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.background(MatchColors.background)
|
||||
.foregroundStyle(.white)
|
||||
// Chrome custom: nasconde la navigation bar di sistema (evita chevron indietro spurî).
|
||||
.toolbar(.hidden, for: .navigationBar)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct MatchSecondaryButton: View {
|
||||
.font(MatchTypography.labelLarge)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
.padding(.horizontal, 14)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 52)
|
||||
}
|
||||
|
||||
@@ -77,10 +77,7 @@ struct MatchesScreen: View {
|
||||
Text(L10n.t("matches.subtitle"))
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
HStack(spacing: 12) {
|
||||
MatchSecondaryButton(label: L10n.t("matches.schedule").uppercased(), action: { showSchedule = true })
|
||||
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
|
||||
}
|
||||
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
|
||||
if let activeTeam {
|
||||
TeamPickerBar(
|
||||
team: activeTeam,
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AppNavHost: View {
|
||||
private enum RootScreen {
|
||||
case splash
|
||||
case login
|
||||
case matches
|
||||
}
|
||||
|
||||
@StateObject private var container = AppContainer()
|
||||
@State private var root: RootScreen = .splash
|
||||
/// Solo destinazioni sopra la root (account, recupero password).
|
||||
@State private var path: [Routes] = []
|
||||
@State private var wizardRoute: WizardRoute?
|
||||
@State private var broadcastRoute: BroadcastRoute?
|
||||
@@ -17,49 +25,27 @@ struct AppNavHost: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $path) {
|
||||
SplashScreen(
|
||||
container: container,
|
||||
onAuthenticated: { path = [.matches] },
|
||||
onUnauthenticated: { path = [.login] }
|
||||
)
|
||||
.navigationDestination(for: Routes.self) { route in
|
||||
switch route {
|
||||
case .splash:
|
||||
EmptyView()
|
||||
case .login:
|
||||
LoginScreen(
|
||||
container: container,
|
||||
onLoggedIn: { path = [.matches] },
|
||||
onForgotPassword: { path.append(.forgotPassword) }
|
||||
)
|
||||
case .forgotPassword:
|
||||
ForgotPasswordScreen(
|
||||
container: container,
|
||||
onBack: { path.removeLast() }
|
||||
)
|
||||
case .matches:
|
||||
MatchesScreen(
|
||||
container: container,
|
||||
refreshToken: matchesRefreshToken,
|
||||
onOpenSetup: { matchId in
|
||||
wizardRoute = WizardRoute(matchId: matchId, step: 1)
|
||||
},
|
||||
onOpenBroadcast: { sessionId in
|
||||
broadcastRoute = BroadcastRoute(sessionId: sessionId)
|
||||
},
|
||||
onOpenAccount: { path.append(.account) }
|
||||
)
|
||||
.lockPortraitOrientation()
|
||||
case .account:
|
||||
AccountScreen(
|
||||
container: container,
|
||||
onBack: { path.removeLast() },
|
||||
onLoggedOut: { path = [.login] }
|
||||
)
|
||||
case .setup, .broadcast:
|
||||
EmptyView()
|
||||
rootContent
|
||||
.navigationDestination(for: Routes.self) { route in
|
||||
switch route {
|
||||
case .forgotPassword:
|
||||
ForgotPasswordScreen(
|
||||
container: container,
|
||||
onBack: { path.removeLast() }
|
||||
)
|
||||
case .account:
|
||||
AccountScreen(
|
||||
container: container,
|
||||
onBack: { path.removeLast() },
|
||||
onLoggedOut: {
|
||||
path = []
|
||||
root = .login
|
||||
}
|
||||
)
|
||||
case .splash, .login, .matches, .setup, .broadcast:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(\.locale, appLocale)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
|
||||
@@ -98,7 +84,8 @@ struct AppNavHost: View {
|
||||
AppOrientation.lockPortrait()
|
||||
container.wizardSession.reset()
|
||||
broadcastRoute = nil
|
||||
path = [.matches]
|
||||
path = []
|
||||
root = .matches
|
||||
}
|
||||
.keepScreenOn()
|
||||
}
|
||||
@@ -109,4 +96,44 @@ struct AppNavHost: View {
|
||||
}
|
||||
.environmentObject(container)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var rootContent: some View {
|
||||
switch root {
|
||||
case .splash:
|
||||
SplashScreen(
|
||||
container: container,
|
||||
onAuthenticated: {
|
||||
path = []
|
||||
root = .matches
|
||||
},
|
||||
onUnauthenticated: {
|
||||
path = []
|
||||
root = .login
|
||||
}
|
||||
)
|
||||
case .login:
|
||||
LoginScreen(
|
||||
container: container,
|
||||
onLoggedIn: {
|
||||
path = []
|
||||
root = .matches
|
||||
},
|
||||
onForgotPassword: { path.append(.forgotPassword) }
|
||||
)
|
||||
case .matches:
|
||||
MatchesScreen(
|
||||
container: container,
|
||||
refreshToken: matchesRefreshToken,
|
||||
onOpenSetup: { matchId in
|
||||
wizardRoute = WizardRoute(matchId: matchId, step: 1)
|
||||
},
|
||||
onOpenBroadcast: { sessionId in
|
||||
broadcastRoute = BroadcastRoute(sessionId: sessionId)
|
||||
},
|
||||
onOpenAccount: { path.append(.account) }
|
||||
)
|
||||
.lockPortraitOrientation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ lines += [
|
||||
app_settings = """
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
@@ -107,7 +107,7 @@ app_settings = """
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.5;
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
API_BASE_URL = "https://www.matchlivetv.it";
|
||||
@@ -122,10 +122,10 @@ app_debug_settings = app_settings + """
|
||||
test_settings = """
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MARKETING_VERSION = 2.0.5;
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
Reference in New Issue
Block a user