[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.
This commit is contained in:
Daniel Arantes Loverde
2026-07-09 09:55:36 -03:00
parent 83b991c0f0
commit 91c53c90d5
5 changed files with 168 additions and 62 deletions

View File

@@ -339,14 +339,6 @@
"comment" : "A prompt asking users to share their opinion about the delivery.", "comment" : "A prompt asking users to share their opinion about the delivery.",
"isCommentAutoGenerated" : true "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" : { "Criar conta" : {
"comment" : "A link that navigates to the registration screen.", "comment" : "A link that navigates to the registration screen.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@@ -748,6 +740,9 @@
"Não" : { "Não" : {
"comment" : "The label of a button in an alert that says \"No\".", "comment" : "The label of a button in an alert that says \"No\".",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Não foi possível carregar %@." : {
}, },
"Não recebeu o código?" : { "Não recebeu o código?" : {
"comment" : "A question displayed below the button to re-send the OTP.", "comment" : "A question displayed below the button to re-send the OTP.",

View File

@@ -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)
}
}

View File

@@ -1,22 +1,117 @@
import SwiftUI import SwiftUI
import WebKit
struct TermsOfUseView: View { private struct RemotePDFView: View {
@Environment(\.colorScheme) var colorScheme let url: URL
@Environment(\.dismiss) var dismiss @Binding var isLoading: Bool
@Binding var hasError: Bool
var body: some View { var body: some View {
ScrollView { RemotePDFRepresentable(url: url, isLoading: $isLoading, hasError: $hasError)
VStack(alignment: .leading, spacing: 16) { }
}
#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<Bool>, hasError: Binding<Bool>) {
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 screenHeader
Text("Termos de Uso") .padding(24)
.font(AppTypography.heading1)
.foregroundStyle(AppColors.textPrimary) ZStack {
Text("Conteúdo dos termos de uso...") 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) .font(AppTypography.body)
.foregroundStyle(AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
.multilineTextAlignment(.center)
} }
.padding(24) .padding(24)
} }
}
}
.background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()) .background((colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea())
.appHiddenNavigationBar() .appHiddenNavigationBar()
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
@@ -24,7 +119,7 @@ struct TermsOfUseView: View {
private var screenHeader: some View { private var screenHeader: some View {
ZStack { ZStack {
Text("Termos de Uso") Text(headerTitle)
.font(AppTypography.heading2) .font(AppTypography.heading2)
.foregroundStyle(AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
HStack { 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 { struct PrivacyPolicyView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
ScrollView { LegalDocumentScreen(title: "a Política de Privacidade", headerTitle: "Privacidade", document: .privacyPolicy)
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()
}
}
} }
} }

View File

@@ -90,6 +90,26 @@ struct ProfileView: View {
Text("Versão 1.0b") Text("Versão 1.0b")
.font(.system(size: 16, weight: .medium)) .font(.system(size: 16, weight: .medium))
.foregroundStyle(AppColors.textMuted) .foregroundStyle(AppColors.textMuted)
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) .padding(.bottom, 12)
} }
} }

View File

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