Base fixes and test harness

This commit is contained in:
alexandrev-tibco
2026-02-01 11:12:57 +01:00
parent f5b13ec924
commit e328767c4a
39 changed files with 3575 additions and 142 deletions
+97 -58
View File
@@ -2,6 +2,7 @@ import Foundation
import SwiftUI
import Combine
import GoogleMobileAds
import UserMessagingPlatform
import AppTrackingTransparency
import AdSupport
@@ -12,6 +13,7 @@ class AdMobService: ObservableObject {
@Published var isConsentObtained = false
@Published var canShowAds = false
@Published var isLoading = false
@Published var shouldRequestNonPersonalizedAds = false
// MARK: - Ad Unit IDs
@@ -25,76 +27,107 @@ class AdMobService: ObservableObject {
// MARK: - Initialization
init() {
checkConsentStatus()
setupResetObserver()
Task {
await configureConsentAndRequestAdsIfNeeded()
}
}
// MARK: - Consent Management
func checkConsentStatus() {
// Check if we already have consent
let consentStatus = UserDefaults.standard.bool(forKey: "adConsentObtained")
isConsentObtained = consentStatus
canShowAds = consentStatus
}
func requestConsent() async {
if #available(iOS 14.5, *) {
let status = await ATTrackingManager.requestTrackingAuthorization()
await configureConsentAndRequestAdsIfNeeded()
}
switch status {
case .authorized:
isConsentObtained = true
canShowAds = true
case .denied, .restricted:
// Can still show non-personalized ads
isConsentObtained = true
canShowAds = true
case .notDetermined:
// Will be asked again later
break
@unknown default:
break
func presentPrivacyOptions() async {
guard let root = topViewController() else { return }
await withCheckedContinuation { continuation in
ConsentForm.presentPrivacyOptionsForm(from: root) { _ in
continuation.resume()
}
} else {
// iOS 14.4 and earlier - consent assumed
isConsentObtained = true
canShowAds = true
}
UserDefaults.standard.set(isConsentObtained, forKey: "adConsentObtained")
updateConsentState()
}
// MARK: - GDPR Consent (UMP SDK)
func requestGDPRConsent() async {
// Implement UMP SDK consent flow if targeting EU users
// This is a simplified version - full implementation requires UMP SDK
let isEUUser = isUserInEU()
if isEUUser {
// Show GDPR consent dialog
// For now, assume consent if user continues
isConsentObtained = true
canShowAds = true
} else {
isConsentObtained = true
canShowAds = true
}
UserDefaults.standard.set(isConsentObtained, forKey: "adConsentObtained")
func resetConsent() {
ConsentInformation.shared.reset()
UserDefaults.standard.removeObject(forKey: AppConstants.StorageKeys.adConsentObtained)
isConsentObtained = false
canShowAds = false
shouldRequestNonPersonalizedAds = false
}
private func isUserInEU() -> Bool {
let euCountries = [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
"PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", "IS", "LI",
"NO", "CH"
]
private func setupResetObserver() {
NotificationCenter.default.addObserver(
forName: .didResetData,
object: nil,
queue: .main
) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
self.resetConsent()
await self.configureConsentAndRequestAdsIfNeeded()
}
}
}
let countryCode = Locale.current.region?.identifier ?? ""
return euCountries.contains(countryCode)
private func configureConsentAndRequestAdsIfNeeded() async {
isLoading = true
let parameters = RequestParameters()
await withCheckedContinuation { continuation in
ConsentInformation.shared.requestConsentInfoUpdate(with: parameters) { _ in
continuation.resume()
}
}
if let root = topViewController() {
await withCheckedContinuation { continuation in
ConsentForm.loadAndPresentIfRequired(from: root) { _ in
continuation.resume()
}
}
}
updateConsentState()
if canShowAds {
await requestTrackingIfNeeded()
updateConsentState()
}
isLoading = false
}
private func updateConsentState() {
let consentInfo = ConsentInformation.shared
canShowAds = consentInfo.canRequestAds
isConsentObtained = consentInfo.consentStatus == .obtained || consentInfo.consentStatus == .notRequired
let trackingDenied: Bool
if #available(iOS 14.5, *) {
trackingDenied = ATTrackingManager.trackingAuthorizationStatus != .authorized
} else {
trackingDenied = false
}
shouldRequestNonPersonalizedAds = !isConsentObtained || trackingDenied
UserDefaults.standard.set(isConsentObtained, forKey: AppConstants.StorageKeys.adConsentObtained)
}
private func requestTrackingIfNeeded() async {
if #available(iOS 14.5, *) {
_ = await ATTrackingManager.requestTrackingAuthorization()
}
}
private func topViewController() -> UIViewController? {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController else {
return nil
}
var current = root
while let presented = current.presentedViewController {
current = presented
}
return current
}
// MARK: - Analytics
@@ -159,7 +192,13 @@ struct BannerAdView: UIViewRepresentable {
}
bannerView.delegate = context.coordinator
bannerView.load(Request())
let request = Request()
if adMobService.shouldRequestNonPersonalizedAds {
let extras = Extras()
extras.additionalParameters = ["npa": "1"]
request.register(extras)
}
bannerView.load(request)
return bannerView
}
@@ -376,7 +376,7 @@ class CalculationService {
sources: [InvestmentSource],
totalPortfolioValue: Decimal
) -> [CategoryMetrics] {
categories.map { category in
let rawMetrics = categories.map { category in
let categorySources = sources.filter { $0.category?.id == category.id }
let allSnapshots = categorySources.flatMap { $0.snapshotsArray }
let metrics = calculateCategoryMetrics(from: allSnapshots)
@@ -396,6 +396,24 @@ class CalculationService {
metrics: metrics
)
}
let filtered = rawMetrics.filter { metric in
metric.totalValue > 0 && sources.contains { $0.category?.id == metric.id }
}
var deduped: [String: CategoryMetrics] = [:]
for metric in filtered {
let key = metric.categoryName.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if let existing = deduped[key] {
if metric.totalValue > existing.totalValue {
deduped[key] = metric
}
} else {
deduped[key] = metric
}
}
return Array(deduped.values)
}
private struct SeriesPoint {