diff --git a/DashWallet/AppDelegate.m b/DashWallet/AppDelegate.m index b0616620c..cfaedcd45 100644 --- a/DashWallet/AppDelegate.m +++ b/DashWallet/AppDelegate.m @@ -197,8 +197,8 @@ - (void)applicationDidBecomeActive:(UIApplication *)application { // [self.balanceNotifier updateBalance]; - // Check geo-restriction for PiggyCards (if available) - // This logs location info each time the app becomes active for debugging + // Check geo-restriction for PiggyCards (if available). No-ops once the + // country has been resolved for this launch. SEL checkGeoRestrictionSelector = NSSelectorFromString(@"checkGeoRestriction"); if ([ExploreDashObjcWrapper respondsToSelector:checkGeoRestrictionSelector]) { #pragma clang diagnostic push diff --git a/DashWallet/Sources/Models/Explore Dash/ExploreDash.swift b/DashWallet/Sources/Models/Explore Dash/ExploreDash.swift index c440f2413..b58e856a9 100644 --- a/DashWallet/Sources/Models/Explore Dash/ExploreDash.swift +++ b/DashWallet/Sources/Models/Explore Dash/ExploreDash.swift @@ -83,7 +83,9 @@ public class ExploreDashObjcWrapper: NSObject { } #if PIGGYCARDS_ENABLED - /// Check geo-restriction status. Call this when the app becomes active to log location info. + /// Resolve the user's country if it has not been resolved yet this launch. + /// Cheap to call repeatedly: `checkRestriction()` returns immediately once a + /// country has been determined. @objc public class func checkGeoRestriction() { Task { @MainActor in await GeoRestrictionService.shared.checkRestriction() diff --git a/DashWallet/Sources/Models/Explore Dash/Services/GeoRestrictionService.swift b/DashWallet/Sources/Models/Explore Dash/Services/GeoRestrictionService.swift index 1ed27711e..b685318bb 100644 --- a/DashWallet/Sources/Models/Explore Dash/Services/GeoRestrictionService.swift +++ b/DashWallet/Sources/Models/Explore Dash/Services/GeoRestrictionService.swift @@ -41,8 +41,11 @@ func isPiggyCardsGeoRestricted() -> Bool { class GeoRestrictionService { static let shared = GeoRestrictionService() - /// Country codes that are restricted from using PiggyCards - private let restrictedCountryCodes: Set = ["RU", "CU"] + /// Country codes that are restricted from using PiggyCards, listed in both + /// ISO 3166-1 alpha-2 and alpha-3 form because the sources disagree: + /// CoreLocation's `isoCountryCode` and `Locale.Region` are alpha-2, while + /// StoreKit's `Storefront.countryCode` is alpha-3. + private let restrictedCountryCodes: Set = ["RU", "RUS", "CU", "CUB"] /// UserDefaults keys for thread-safe access private enum Keys { @@ -75,8 +78,8 @@ class GeoRestrictionService { enum DetectionSource: String { case gpsLocation = "GPS Location" - case ipGeolocation = "IP Geolocation" case appStore = "App Store" + case deviceRegion = "Device Region" case unknown = "Unknown" } @@ -91,11 +94,6 @@ class GeoRestrictionService { detectionSource = DetectionSource(rawValue: sourceString) } - DWLogger.log("🌍 GeoRestrictionService: Initialized") - DWLogger.log("🌍 GeoRestrictionService: Persisted restriction status: \(isPiggyCardsRestricted)") - DWLogger.log("🌍 GeoRestrictionService: Persisted country code: \(detectedCountryCode ?? "nil")") - DWLogger.log("🌍 GeoRestrictionService: Persisted detection source: \(detectionSource?.rawValue ?? "nil")") - // Listen for location changes to update restriction status setupLocationObserver() } @@ -111,90 +109,49 @@ class GeoRestrictionService { .store(in: &cancellables) } - /// Check if PiggyCards is restricted for the current user - /// This should be called when the app needs to determine PiggyCards availability + /// Resolve the user's country and cache the resulting restriction status. + /// + /// Every source consulted here is local to the device — none of them + /// contacts a remote service, so opening the app does not disclose the + /// user's IP address to a third party. + /// + /// Runs at most once per launch: the first call that resolves a country + /// sets `hasCheckedRestriction`. Later GPS fixes still refresh the status + /// through `setupLocationObserver`, and `refreshRestriction()` forces a + /// re-check. func checkRestriction() async { - DWLogger.log("🌍 GeoRestrictionService: ========== LOCATION CHECK START ==========") - - // Gather all location attributes for logging - let gpsAuthorized = DWLocationManager.shared.isAuthorized - let gpsCountry = DWLocationManager.shared.currentPlacemark?.isoCountryCode - let ipCountry = await fetchCountryFromIP() - let appStoreCountry = await fetchAppStoreCountry() - - DWLogger.log("🌍 GeoRestrictionService: 1. GPS Location:") - DWLogger.log("🌍 - Permission granted: \(gpsAuthorized)") - DWLogger.log("🌍 - Country code: \(gpsCountry ?? "nil")") - - DWLogger.log("🌍 GeoRestrictionService: 2. IP Geolocation:") - DWLogger.log("🌍 - Country code: \(ipCountry ?? "nil")") - - DWLogger.log("🌍 GeoRestrictionService: 3. App Store:") - DWLogger.log("🌍 - Country code: \(appStoreCountry ?? "nil")") + guard !hasCheckedRestriction else { return } - DWLogger.log("🌍 GeoRestrictionService: ========================================") - - // Priority: GPS -> IP -> App Store - if gpsAuthorized, let countryCode = gpsCountry { - DWLogger.log("🌍 GeoRestrictionService: ✅ Using GPS location: \(countryCode)") + // Priority: GPS -> App Store storefront -> device region. + if DWLocationManager.shared.isAuthorized, + let countryCode = DWLocationManager.shared.currentPlacemark?.isoCountryCode { updateRestrictionStatus(countryCode: countryCode, source: .gpsLocation) return } - if let countryCode = ipCountry { - DWLogger.log("🌍 GeoRestrictionService: ✅ Using IP geolocation: \(countryCode)") - updateRestrictionStatus(countryCode: countryCode, source: .ipGeolocation) + if let countryCode = await fetchAppStoreCountry() { + updateRestrictionStatus(countryCode: countryCode, source: .appStore) return } - if let countryCode = appStoreCountry { - DWLogger.log("🌍 GeoRestrictionService: ✅ Using App Store country: \(countryCode)") - updateRestrictionStatus(countryCode: countryCode, source: .appStore) + if let countryCode = Locale.current.region?.identifier { + updateRestrictionStatus(countryCode: countryCode, source: .deviceRegion) return } - // If all methods fail, assume not restricted - DWLogger.log("🌍 GeoRestrictionService: ⚠️ Unable to determine country, assuming not restricted") + // Nothing resolved. Leave `hasCheckedRestriction` false so a later call + // can retry once a source becomes available. + DWLogger.log("GeoRestrictionService: country undetermined, leaving PiggyCards unrestricted") isPiggyCardsRestricted = false detectedCountryCode = nil detectionSource = .unknown } - /// Fetch country code from IP geolocation service - private func fetchCountryFromIP() async -> String? { - // Using ip-api.com - free, no API key required, returns country code - guard let url = URL(string: "https://ip-api.com/json/?fields=countryCode") else { - return nil - } - - do { - let (data, response) = try await URLSession.shared.data(from: url) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - DWLogger.log("GeoRestrictionService: IP geolocation request failed") - return nil - } - - if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let countryCode = json["countryCode"] as? String { - DWLogger.log("GeoRestrictionService: IP geolocation returned country: \(countryCode)") - return countryCode - } - } catch { - DWLogger.log("GeoRestrictionService: IP geolocation error: \(error.localizedDescription)") - } - - return nil - } - - /// Fetch country code from App Store storefront + /// Fetch country code from App Store storefront. Returns an ISO 3166-1 + /// alpha-3 code, which `restrictedCountryCodes` accounts for. private func fetchAppStoreCountry() async -> String? { - if let storefront = await Storefront.current { - DWLogger.log("GeoRestrictionService: App Store country: \(storefront.countryCode)") - return storefront.countryCode - } - return nil + guard let storefront = await Storefront.current else { return nil } + return storefront.countryCode } /// Update the restriction status based on detected country @@ -202,18 +159,12 @@ class GeoRestrictionService { let normalizedCode = countryCode.uppercased() let isRestricted = restrictedCountryCodes.contains(normalizedCode) - DWLogger.log("🌍 GeoRestrictionService: updateRestrictionStatus called") - DWLogger.log("🌍 GeoRestrictionService: Country code: \(normalizedCode)") - DWLogger.log("🌍 GeoRestrictionService: Source: \(source.rawValue)") - DWLogger.log("🌍 GeoRestrictionService: Is restricted country: \(isRestricted)") - DWLogger.log("🌍 GeoRestrictionService: Restricted countries list: \(restrictedCountryCodes)") - self.detectedCountryCode = normalizedCode self.detectionSource = source self.isPiggyCardsRestricted = isRestricted self.hasCheckedRestriction = true - DWLogger.log("🌍 GeoRestrictionService: ✅ Restriction status updated - isPiggyCardsRestricted = \(isRestricted)") + DWLogger.log("GeoRestrictionService: \(normalizedCode) via \(source.rawValue), PiggyCards restricted: \(isRestricted)") } /// Force refresh the restriction check