Files
MatchLiveTv/native/ios/MatchLiveTv/UI/Login/LoginScreen.swift
T
eminuxandCursor 5b723bf844 Aggiunge annunci in-app/sito, contatti e migliora UI copertina sul portale.
Include admin/API/banner nativi, form contatti e reset copertina standard con layout edit più ampio.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 15:30:49 +02:00

166 lines
6.3 KiB
Swift

import SwiftUI
struct LoginScreen: View {
@ObservedObject var container: AppContainer
let onLoggedIn: () -> Void
let onForgotPassword: () -> Void
@State private var email = ""
@State private var password = ""
@State private var error: String?
@State private var loading = false
@State private var passwordVisible = false
@State private var showLanguagePicker = false
@State private var languageTick = 0
@State private var announcements: [AnnouncementDto] = []
var body: some View {
MatchScreenScaffold {
ScrollView {
VStack(spacing: 0) {
HStack {
Spacer()
Button {
showLanguagePicker = true
} label: {
Image(systemName: "globe")
.foregroundStyle(MatchColors.textSecondary)
}
.accessibilityLabel(L10n.t("language.label"))
}
.padding(.top, 8)
MatchLiveWordmark(showSlogan: true)
.padding(.top, 16)
if !announcements.isEmpty {
AnnouncementBanners(
announcements: announcements,
onDismiss: { id in
container.announcementStore.dismiss(id)
announcements.removeAll { $0.id == id }
}
)
.padding(.top, 24)
}
Text(L10n.t("login.submit").uppercased())
.font(MatchTypography.headlineMedium)
.padding(.top, 32)
VStack(spacing: 16) {
MatchTextField(title: L10n.t("login.email"), text: $email, placeholder: L10n.t("login.email.placeholder"), keyboard: .emailAddress)
MatchSecureField(title: L10n.t("login.password"), text: $password, visible: $passwordVisible)
}
.padding(.top, 32)
HStack {
Spacer()
Button(action: onForgotPassword) {
Text(L10n.t("login.forgot.password"))
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
.underline()
}
}
.padding(.top, 12)
if let error {
Text(error)
.foregroundStyle(MatchColors.primaryRed)
.multilineTextAlignment(.center)
.padding(.top, 12)
}
MatchPrimaryButton(
label: L10n.t("login.submit").uppercased(),
action: submitLogin,
enabled: !email.isEmpty && !password.isEmpty,
loading: loading
)
.padding(.top, 32)
}
.padding(.horizontal, 24)
.padding(.bottom, 32)
}
}
.sheet(isPresented: $showLanguagePicker) {
LanguagePickerView()
}
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
languageTick += 1
}
.id(languageTick)
.task {
announcements = await container.visibleAnnouncements()
}
}
private func submitLogin() {
guard !loading, !email.isEmpty, !password.isEmpty else { return }
loading = true
error = nil
Task {
do {
_ = try await container.authRepository.login(email: email.trimmingCharacters(in: .whitespaces), password: password)
onLoggedIn()
} catch {
if let apiError = error as? APIError, case .unauthorized = apiError {
self.error = L10n.t("login.error.credentials")
} else if let message = UserFacingError.message(for: error),
message.localizedCaseInsensitiveContains("timeout") {
self.error = L10n.t("login.error.unreachable")
} else {
self.error = UserFacingError.message(for: error) ?? L10n.t("login.error.generic")
}
}
loading = false
}
}
}
private struct MatchTextField: View {
let title: String
@Binding var text: String
var placeholder: String = ""
var keyboard: UIKeyboardType = .default
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
TextField(placeholder, text: $text)
.textInputAutocapitalization(.never)
.keyboardType(keyboard)
.autocorrectionDisabled()
.padding(12)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
}
}
}
private struct MatchSecureField: View {
let title: String
@Binding var text: String
@Binding var visible: Bool
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
HStack {
Group {
if visible {
TextField("", text: $text)
} else {
SecureField("", text: $text)
}
}
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Button(action: { visible.toggle() }) {
Image(systemName: visible ? "eye.slash" : "eye")
.foregroundStyle(MatchColors.textSecondary)
}
}
.padding(12)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
}
}
}