From 91c53c90d5b1cbec81f61f673e52a9abdf75a6e2 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Thu, 9 Jul 2026 09:55:36 -0300 Subject: [PATCH] [legal-api-links] Load Terms of Use and Privacy Policy from Atomenta API Terms and Privacy screens now fetch the real PediFoods customer PDFs from the backend instead of showing static placeholder text. Also surfaces both links on the Profile screen below the version label, not just during registration. --- .../PediFoods/Resources/Localizable.xcstrings | 11 +- Sources/PediFoods/Services/ApiConfig.swift | 16 ++ Sources/PediFoods/Views/Auth/LegalViews.swift | 169 ++++++++++++------ .../PediFoods/Views/Main/ProfileView.swift | 22 ++- Tests/PediFoodsTests/LegalDocumentTests.swift | 12 ++ 5 files changed, 168 insertions(+), 62 deletions(-) create mode 100644 Tests/PediFoodsTests/LegalDocumentTests.swift diff --git a/Sources/PediFoods/Resources/Localizable.xcstrings b/Sources/PediFoods/Resources/Localizable.xcstrings index 1680d5b..e776746 100644 --- a/Sources/PediFoods/Resources/Localizable.xcstrings +++ b/Sources/PediFoods/Resources/Localizable.xcstrings @@ -339,14 +339,6 @@ "comment" : "A prompt asking users to share their opinion about the delivery.", "isCommentAutoGenerated" : true }, - "Conteúdo da política de privacidade..." : { - "comment" : "A placeholder text describing the content of the privacy policy.", - "isCommentAutoGenerated" : true - }, - "Conteúdo dos termos de uso..." : { - "comment" : "A placeholder text describing the content of the terms of use.", - "isCommentAutoGenerated" : true - }, "Criar conta" : { "comment" : "A link that navigates to the registration screen.", "isCommentAutoGenerated" : true @@ -748,6 +740,9 @@ "Não" : { "comment" : "The label of a button in an alert that says \"No\".", "isCommentAutoGenerated" : true + }, + "Não foi possível carregar %@." : { + }, "Não recebeu o código?" : { "comment" : "A question displayed below the button to re-send the OTP.", diff --git a/Sources/PediFoods/Services/ApiConfig.swift b/Sources/PediFoods/Services/ApiConfig.swift index 2c67a63..4c6366a 100644 --- a/Sources/PediFoods/Services/ApiConfig.swift +++ b/Sources/PediFoods/Services/ApiConfig.swift @@ -39,3 +39,19 @@ enum ApiConfig { } } } + +enum LegalDocument: Sendable { + case terms + case privacyPolicy + + private var path: String { + switch self { + case .terms: return "api/public/pedi-foods-customer/terms" + case .privacyPolicy: return "api/public/pedi-foods-customer/privacy-policy" + } + } + + var url: URL { + ApiConfig.baseURL.appendingPathComponent(path) + } +} diff --git a/Sources/PediFoods/Views/Auth/LegalViews.swift b/Sources/PediFoods/Views/Auth/LegalViews.swift index 9c40302..a504b53 100644 --- a/Sources/PediFoods/Views/Auth/LegalViews.swift +++ b/Sources/PediFoods/Views/Auth/LegalViews.swift @@ -1,21 +1,116 @@ import SwiftUI +import WebKit -struct TermsOfUseView: View { - @Environment(\.colorScheme) var colorScheme - @Environment(\.dismiss) var dismiss +private struct RemotePDFView: View { + let url: URL + @Binding var isLoading: Bool + @Binding var hasError: Bool var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - screenHeader - Text("Termos de Uso") - .font(AppTypography.heading1) - .foregroundStyle(AppColors.textPrimary) - Text("Conteúdo dos termos de uso...") - .font(AppTypography.body) - .foregroundStyle(AppColors.textPrimary) + RemotePDFRepresentable(url: url, isLoading: $isLoading, hasError: $hasError) + } +} + +#if canImport(UIKit) +private struct RemotePDFRepresentable: UIViewRepresentable { + let url: URL + @Binding var isLoading: Bool + @Binding var hasError: Bool + + func makeUIView(context: Context) -> WKWebView { + let webView = WKWebView() + webView.navigationDelegate = context.coordinator + webView.load(URLRequest(url: url)) + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) {} + + func makeCoordinator() -> RemotePDFCoordinator { + RemotePDFCoordinator(isLoading: $isLoading, hasError: $hasError) + } +} +#elseif canImport(AppKit) +private struct RemotePDFRepresentable: NSViewRepresentable { + let url: URL + @Binding var isLoading: Bool + @Binding var hasError: Bool + + func makeNSView(context: Context) -> WKWebView { + let webView = WKWebView() + webView.navigationDelegate = context.coordinator + webView.load(URLRequest(url: url)) + return webView + } + + func updateNSView(_ nsView: WKWebView, context: Context) {} + + func makeCoordinator() -> RemotePDFCoordinator { + RemotePDFCoordinator(isLoading: $isLoading, hasError: $hasError) + } +} +#endif + +private final class RemotePDFCoordinator: NSObject, WKNavigationDelegate { + @Binding var isLoading: Bool + @Binding var hasError: Bool + + init(isLoading: Binding, hasError: Binding) { + self._isLoading = isLoading + self._hasError = hasError + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + isLoading = true + hasError = false + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + isLoading = false + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + isLoading = false + hasError = true + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + isLoading = false + hasError = true + } +} + +private struct LegalDocumentScreen: View { + let title: String + let headerTitle: String + let document: LegalDocument + @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss + @State private var isLoading = true + @State private var hasError = false + + var body: some View { + VStack(spacing: 0) { + screenHeader + .padding(24) + + ZStack { + RemotePDFView(url: document.url, isLoading: $isLoading, hasError: $hasError) + + if isLoading { + ProgressView() + } + + if hasError { + VStack(spacing: 8) { + Text("Não foi possível carregar \(title.lowercased()).") + .font(AppTypography.body) + .foregroundStyle(AppColors.textPrimary) + .multilineTextAlignment(.center) + } + .padding(24) + } } - .padding(24) } .background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()) .appHiddenNavigationBar() @@ -24,7 +119,7 @@ struct TermsOfUseView: View { private var screenHeader: some View { ZStack { - Text("Termos de Uso") + Text(headerTitle) .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) HStack { @@ -44,46 +139,14 @@ struct TermsOfUseView: View { } } +struct TermsOfUseView: View { + var body: some View { + LegalDocumentScreen(title: "os Termos de Uso", headerTitle: "Termos de Uso", document: .terms) + } +} + struct PrivacyPolicyView: View { - @Environment(\.colorScheme) var colorScheme - @Environment(\.dismiss) var dismiss - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - screenHeader - Text("Política de Privacidade") - .font(AppTypography.heading1) - .foregroundStyle(AppColors.textPrimary) - Text("Conteúdo da política de privacidade...") - .font(AppTypography.body) - .foregroundStyle(AppColors.textPrimary) - } - .padding(24) - } - .background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()) - .appHiddenNavigationBar() - .navigationBarBackButtonHidden(true) - } - - private var screenHeader: some View { - ZStack { - Text("Privacidade") - .font(AppTypography.heading2) - .foregroundStyle(AppColors.textPrimary) - HStack { - Button(action: { dismiss() }) { - Image(systemName: "chevron.left") - .font(.system(size: 24, weight: .semibold)) - .foregroundStyle(AppColors.textPrimary) - .frame(width: 52, height: 52) - .background(AppColors.surface) - .clipShape(Circle()) - .shadow(color: .black.opacity(0.06), radius: 8, y: 2) - } - .buttonStyle(.plain) - Spacer() - } - } + LegalDocumentScreen(title: "a Política de Privacidade", headerTitle: "Privacidade", document: .privacyPolicy) } } diff --git a/Sources/PediFoods/Views/Main/ProfileView.swift b/Sources/PediFoods/Views/Main/ProfileView.swift index 148fbc3..0064efb 100644 --- a/Sources/PediFoods/Views/Main/ProfileView.swift +++ b/Sources/PediFoods/Views/Main/ProfileView.swift @@ -90,7 +90,27 @@ struct ProfileView: View { Text("Versão 1.0b") .font(.system(size: 16, weight: .medium)) .foregroundStyle(AppColors.textMuted) - .padding(.bottom, 12) + + HStack(spacing: 6) { + NavigationLink { + TermsOfUseView() + } label: { + Text("Termos de Uso") + } + .buttonStyle(.plain) + + Text("·") + + NavigationLink { + PrivacyPolicyView() + } label: { + Text("Política de Privacidade") + } + .buttonStyle(.plain) + } + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(AppColors.textMuted) + .padding(.bottom, 12) } } .ignoresSafeArea(edges: .top) diff --git a/Tests/PediFoodsTests/LegalDocumentTests.swift b/Tests/PediFoodsTests/LegalDocumentTests.swift new file mode 100644 index 0000000..6d7eb37 --- /dev/null +++ b/Tests/PediFoodsTests/LegalDocumentTests.swift @@ -0,0 +1,12 @@ +import Testing +@testable import PediFoods + +@Test("Terms URL points to PediFoods customer terms endpoint") +func termsURL() { + #expect(LegalDocument.terms.url.absoluteString == "https://atomenta.com.br/api/public/pedi-foods-customer/terms") +} + +@Test("Privacy policy URL points to PediFoods customer privacy-policy endpoint") +func privacyPolicyURL() { + #expect(LegalDocument.privacyPolicy.url.absoluteString == "https://atomenta.com.br/api/public/pedi-foods-customer/privacy-policy") +}