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") +}