Base fixes and test harness
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user