import SwiftUI private enum CardAlert: Identifiable { case generate case scannerError(String) var id: String { switch self { case .generate: return "generate" case .scannerError: return "scanner-error" } } } struct CardView: View { @EnvironmentObject private var model: AppModel @State private var cardNumber = "" @State private var cardScannerPresented = false @State private var activeAlert: CardAlert? @FocusState private var cardNumberFocused: Bool var body: some View { GeometryReader { geometry in let compact = geometry.size.height < 680 VStack(alignment: .leading, spacing: compact ? 10 : 18) { Text(model.text("Witaj, ", "Hello, ") + firstName) .font(compact ? .title.bold() : .largeTitle.bold()) .lineLimit(1) .minimumScaleFactor(0.72) Text(model.text("Pokaż kod karty przy kasie.", "Show your card code at the checkout.")) .foregroundStyle(.secondary) .font(compact ? .subheadline : .body) loyaltyCard(compact: compact) summary(compact: compact) } .padding(.horizontal, 20) .padding(.vertical, compact ? 8 : 14) .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) } .background(KTheme.background) .fullScreenCover(isPresented: $cardScannerPresented) { CardScannerScreen( onScan: acceptScannedCard, onCancel: { cardScannerPresented = false }, onError: { error in cardScannerPresented = false activeAlert = .scannerError(error) } ) .ignoresSafeArea() } .toolbar { ToolbarItemGroup(placement: .keyboard) { Spacer() Button(model.text("Gotowe", "Done")) { cardNumberFocused = false } .fontWeight(.semibold) } } .alert(item: $activeAlert) { alert in switch alert { case .generate: return Alert( title: Text(model.text("Wygenerować nową kartę?", "Create a new card?")), message: Text(model.text( "Do konta zostanie na stałe przypisana nowa karta cyfrowa.", "A new digital card will be permanently assigned to this account." )), primaryButton: .default(Text(model.text("Wygeneruj", "Create"))) { Task { _ = await model.generateCard() } }, secondaryButton: .cancel(Text(model.text("Anuluj", "Cancel"))) ) case let .scannerError(message): return Alert( title: Text(model.text("Nie udało się odczytać karty", "Could not read the card")), message: Text(message), dismissButton: .cancel(Text("OK")) ) } } } private var firstName: String { model.loyalty?.firstName?.value.nonEmpty ?? model.user?.firstName?.nonEmpty ?? model.text("Kliencie", "Customer") } private func loyaltyCard(compact: Bool) -> some View { return VStack(spacing: compact ? 10 : 20) { Image("Logo") .resizable() .scaledToFit() .frame(maxHeight: compact ? 54 : 76) if model.cardNumber.count == 13 { VStack(spacing: compact ? 4 : 8) { CardBarcodeView(digits: model.cardNumber) .frame(height: compact ? 108 : 126) Text(model.cardNumber) .font(.system(.headline, design: .monospaced).weight(.bold)) .tracking(2) .foregroundStyle(.black) } .padding(compact ? 8 : 12) .background(.white) .clipShape(RoundedRectangle(cornerRadius: 12)) } else { VStack(alignment: .leading, spacing: 10) { Text(model.text("Przypisz kartę", "Assign a card")).font(.headline) Text(model.text("Wpisz 13-cyfrowy numer znajdujący się na karcie.", "Enter the 13-digit number shown on the card.")) .font(.subheadline) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) TextField(model.text("Numer karty", "Card number"), text: $cardNumber) .keyboardType(.numberPad) .focused($cardNumberFocused) .foregroundStyle(.white) .tint(.white) .padding(.horizontal, 14) .frame(minHeight: 50) .background(KTheme.panel.opacity(0.92)) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.35), lineWidth: 1) } .onChange(of: cardNumber) { value in cardNumber = String(value.filter(\.isNumber).prefix(13)) } Button { Task { if await model.assignCard(cardNumber) { cardNumber = "" } } } label: { Text(model.text("Przypisz", "Assign")) .frame(maxWidth: .infinity, minHeight: 22) } .buttonStyle(.borderedProminent) .tint(KTheme.pink) Button { cardNumberFocused = false cardScannerPresented = true } label: { Label(model.text("Skanuj kartę", "Scan card"), systemImage: "camera.viewfinder") .frame(maxWidth: .infinity, minHeight: 22) } .buttonStyle(.bordered) .tint(.white) .font(.subheadline.weight(.semibold)) Button { cardNumberFocused = false activeAlert = .generate } label: { Text(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one")) .frame(maxWidth: .infinity, minHeight: 22) } .buttonStyle(.bordered) .tint(.white) .font(.subheadline.weight(.semibold)) } } } .frame(maxWidth: .infinity) .padding(compact ? 14 : 22) .background(LinearGradient(colors: [KTheme.orange, KTheme.pink, KTheme.purple], startPoint: .topLeading, endPoint: .bottomTrailing)) .clipShape(RoundedRectangle(cornerRadius: 26, style: .continuous)) .shadow(color: KTheme.pink.opacity(0.2), radius: 18, y: 8) } private func acceptScannedCard(_ scan: ScannedCard) { cardScannerPresented = false let digits = scan.value.filter(\.isNumber) guard digits.count == 13 else { activeAlert = .scannerError(model.text( "Odczytany kod nie zawiera 13 cyfr. Ustaw całą kartę w ramce i spróbuj ponownie.", "The scanned code does not contain 13 digits. Place the whole card inside the frame and try again." )) return } cardNumber = digits } private func summary(compact: Bool) -> some View { VStack(spacing: 0) { Text(model.text("PODSUMOWANIE", "SUMMARY")) .font(.caption.bold()) .foregroundStyle(KTheme.orange) .frame(maxWidth: .infinity, alignment: .leading) .padding(.bottom, 8) StatRow(label: model.text("Punkty", "Points"), value: model.pointsWithValue(model.loyalty?.points?.value), highlighted: true, compact: compact) Divider().overlay(Color.white.opacity(0.15)) StatRow(label: model.text("Wartość zakupów", "Purchase value"), value: model.money(model.loyalty?.transactionsValue?.value), compact: compact) Divider().overlay(Color.white.opacity(0.15)) StatRow(label: model.text("Wartość upustu", "Discount value"), value: model.money(model.loyalty?.discountValue?.value), compact: compact) if let savings = model.loyalty?.savings?.value.nonEmpty { Divider().overlay(Color.white.opacity(0.15)) StatRow(label: model.text("Oszczędności", "Savings"), value: model.money(savings), compact: compact) } } .kasztelanPanel() } } private struct StatRow: View { let label: String let value: String var highlighted = false var compact = false var body: some View { HStack { Text(label).foregroundStyle(.secondary) Spacer() Text(value) .font(highlighted ? .title3.bold() : .headline) .foregroundStyle(highlighted ? KTheme.orange : .white) .multilineTextAlignment(.trailing) } .padding(.vertical, compact ? 8 : 13) } } extension String { var nonEmpty: String? { isEmpty ? nil : self } }