[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:
@@ -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.",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
Tests/PediFoodsTests/LegalDocumentTests.swift
Normal file
12
Tests/PediFoodsTests/LegalDocumentTests.swift
Normal 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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user