diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 3c5240a0163..1b3c0ade3e2 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -27,8 +27,7 @@ extension Networking.Account { displayName: .fake(), email: .fake(), username: .fake(), - gravatarUrl: .fake(), - ipCountryCode: .fake() + gravatarUrl: .fake() ) } } diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 260f1360c8b..2386c67326f 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -177,6 +177,8 @@ 261CF1BC255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */; }; 261CF2CB255C50010090D8D3 /* payment-gateway-list-half.json in Resources */ = {isa = PBXBuildFile; fileRef = 261CF2CA255C50010090D8D3 /* payment-gateway-list-half.json */; }; 262E5AD5255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262E5AD4255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift */; }; + 263659DC2A264A3E00607A0D /* IPLocationRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263659DB2A264A3E00607A0D /* IPLocationRemote.swift */; }; + 263659DE2A2694A000607A0D /* IPLocationRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263659DD2A2694A000607A0D /* IPLocationRemoteTests.swift */; }; 263E37D22641ACEA00260D3B /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 263E37D12641ACEA00260D3B /* Codegen */; }; 263E383F2641FF1600260D3B /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 263E383E2641FF1600260D3B /* Codegen */; }; 263E38402641FF1600260D3B /* Codegen in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 263E383E2641FF1600260D3B /* Codegen */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -211,6 +213,7 @@ 268B68FB24C87384007EBF1D /* leaderboards-products.json in Resources */ = {isa = PBXBuildFile; fileRef = 268B68FA24C87384007EBF1D /* leaderboards-products.json */; }; 268B68FD24C87E37007EBF1D /* leaderboards-year-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = 268B68FC24C87E37007EBF1D /* leaderboards-year-alt.json */; }; 268EC45C26C169F600716F5C /* order-with-faulty-attributes.json in Resources */ = {isa = PBXBuildFile; fileRef = 268EC45B26C169F600716F5C /* order-with-faulty-attributes.json */; }; + 26B15E442A269F79000C35E4 /* ip-location.json in Resources */ = {isa = PBXBuildFile; fileRef = 26B15E432A269F79000C35E4 /* ip-location.json */; }; 26B2F74124C1F2C10065CCC8 /* LeaderboardsRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B2F74024C1F2C10065CCC8 /* LeaderboardsRemote.swift */; }; 26B2F74324C545D50065CCC8 /* Leaderboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B2F74224C545D50065CCC8 /* Leaderboard.swift */; }; 26B2F74524C5573F0065CCC8 /* LeaderboardListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B2F74424C5573F0065CCC8 /* LeaderboardListMapper.swift */; }; @@ -1111,6 +1114,8 @@ 261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsGatewayRemoteTests.swift; sourceTree = ""; }; 261CF2CA255C50010090D8D3 /* payment-gateway-list-half.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "payment-gateway-list-half.json"; sourceTree = ""; }; 262E5AD4255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayListMapperTests.swift; sourceTree = ""; }; + 263659DB2A264A3E00607A0D /* IPLocationRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemote.swift; sourceTree = ""; }; + 263659DD2A2694A000607A0D /* IPLocationRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemoteTests.swift; sourceTree = ""; }; 265BCA01243056E3004E53EE /* categories-all.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "categories-all.json"; sourceTree = ""; }; 265EFBDB285257950033BD33 /* Order+Fallbacks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Order+Fallbacks.swift"; sourceTree = ""; }; 26615472242D596B00A31661 /* ProductCategoriesRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCategoriesRemote.swift; sourceTree = ""; }; @@ -1138,6 +1143,7 @@ 268B68FA24C87384007EBF1D /* leaderboards-products.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "leaderboards-products.json"; sourceTree = ""; }; 268B68FC24C87E37007EBF1D /* leaderboards-year-alt.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "leaderboards-year-alt.json"; sourceTree = ""; }; 268EC45B26C169F600716F5C /* order-with-faulty-attributes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-with-faulty-attributes.json"; sourceTree = ""; }; + 26B15E432A269F79000C35E4 /* ip-location.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "ip-location.json"; sourceTree = ""; }; 26B2F74024C1F2C10065CCC8 /* LeaderboardsRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardsRemote.swift; sourceTree = ""; }; 26B2F74224C545D50065CCC8 /* Leaderboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Leaderboard.swift; sourceTree = ""; }; 26B2F74424C5573F0065CCC8 /* LeaderboardListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardListMapper.swift; sourceTree = ""; }; @@ -2076,6 +2082,7 @@ 24F98C5F2502EF8200F49B68 /* FeatureFlagRemoteTests.swift */, 4513382127A8409000AE5E78 /* InboxNotesRemoteTests.swift */, E13BAD5228F8625600217769 /* InAppPurchasesRemoteTests.swift */, + 263659DD2A2694A000607A0D /* IPLocationRemoteTests.swift */, 03EB99892906AB0C00F06A39 /* JustInTimeMessagesRemoteTests.swift */, 26B2F74824C55ACE0065CCC8 /* LeaderboardsRemoteTests.swift */, 020D07BF23D8587700FD9580 /* MediaRemoteTests.swift */, @@ -2230,6 +2237,7 @@ 24F98C512502E79800F49B68 /* FeatureFlagRemote.swift */, E18152BD28F85B5B0011A0EC /* InAppPurchasesRemote.swift */, 4513381F27A8227F00AE5E78 /* InboxNotesRemote.swift */, + 263659DB2A264A3E00607A0D /* IPLocationRemote.swift */, 03EB99872906A78400F06A39 /* JustInTimeMessagesRemote.swift */, 26B2F74024C1F2C10065CCC8 /* LeaderboardsRemote.swift */, B5DAEFEF2180DD5A0002356A /* NotificationsRemote.swift */, @@ -2459,6 +2467,7 @@ DE50296228C609DE00551736 /* jetpack-user-not-connected.json */, 02B41A91296BEB3000FE3311 /* load-site-current-plan-success.json */, 02B41A93296C04BC00FE3311 /* load-site-plans-no-current-plan.json */, + 26B15E432A269F79000C35E4 /* ip-location.json */, EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */, 02B41A8F296BC85800FE3311 /* site-domains.json */, 02935AED29DFFA74001B793E /* site-enable-trial-error-already-upgraded.json */, @@ -3346,6 +3355,7 @@ 026CF624237D839B009563D4 /* product-variations-load-all.json in Resources */, 02AF07EC27492FDD00B2D81E /* media-library-from-wordpress-site.json in Resources */, CC9A253C26442C71005DE56E /* shipping-label-eligibility-success.json in Resources */, + 26B15E442A269F79000C35E4 /* ip-location.json in Resources */, B5A24179217F98F600595DEF /* notifications-load-all.json in Resources */, DEA6B1C9296D0E8B005AA5E9 /* systemStatusWithPluginsOnly-without-data.json in Resources */, 02C4325F298A55D100F14AEE /* domain-contact-info.json in Resources */, @@ -3770,6 +3780,7 @@ 09EA564B27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift in Sources */, CE227093228DD44C00C0626C /* ProductStatus.swift in Sources */, 451A97E9260B657D0059D135 /* ShippingLabelPredefinedOption.swift in Sources */, + 263659DC2A264A3E00607A0D /* IPLocationRemote.swift in Sources */, 02C2548425635BD000A04423 /* ShippingLabelPaperSize.swift in Sources */, CE132BBC223859710029DB6C /* ProductTag.swift in Sources */, DE66C5532976508300DAA978 /* CookieNonceAuthenticator.swift in Sources */, @@ -4241,6 +4252,7 @@ D8FBFF0F22D3B25E006E3336 /* WooAPIVersionTests.swift in Sources */, 45152831257A8E1A0076B03C /* ProductAttributeMapperTests.swift in Sources */, CCA1D60A2943809700B40560 /* SiteSummaryStatsMapperTests.swift in Sources */, + 263659DE2A2694A000607A0D /* IPLocationRemoteTests.swift in Sources */, 26B2F74924C55ACE0065CCC8 /* LeaderboardsRemoteTests.swift in Sources */, 45CCFCE827A2E5020012E8CB /* InboxNoteListMapperTests.swift in Sources */, 74002D6C2118B88200A63C19 /* SiteStatsRemoteTests.swift in Sources */, diff --git a/Networking/Networking/Model/Account.swift b/Networking/Networking/Model/Account.swift index 194fa53370f..074a3372c91 100644 --- a/Networking/Networking/Model/Account.swift +++ b/Networking/Networking/Model/Account.swift @@ -25,22 +25,14 @@ public struct Account: Decodable, Equatable, GeneratedFakeable { /// public let gravatarUrl: String? - /// Users IP country Code - /// This setting is not stored in the Storage layer because we don't want to rely on stale value. - /// But there us no problem on add it later if we believe it will be useful. - /// - public let ipCountryCode: String - - /// Designated Initializer. /// - public init(userID: Int64, displayName: String, email: String, username: String, gravatarUrl: String?, ipCountryCode: String) { + public init(userID: Int64, displayName: String, email: String, username: String, gravatarUrl: String?) { self.userID = userID self.displayName = displayName self.email = email self.username = username self.gravatarUrl = gravatarUrl - self.ipCountryCode = ipCountryCode } } @@ -55,6 +47,5 @@ private extension Account { case email = "email" case username = "username" case gravatarUrl = "avatar_URL" - case ipCountryCode = "user_ip_country_code" } } diff --git a/Networking/Networking/Remote/IPLocationRemote.swift b/Networking/Networking/Remote/IPLocationRemote.swift new file mode 100644 index 00000000000..b89f05a32e6 --- /dev/null +++ b/Networking/Networking/Remote/IPLocationRemote.swift @@ -0,0 +1,46 @@ +import Foundation + +/// Remote type to fetch the user's IP Location using the public `geo` API. +/// +public final class IPLocationRemote: Remote { + + /// Fetches the country code from the device ip. + /// + public func getIPCountryCode(onCompletion: @escaping (Result) -> Void) { + let path = "geo/" // Needs the trailing slash otherwise the request will fail. + guard let url = URL(string: Settings.wordpressApiBaseURL + path) else { + return onCompletion(.failure(IPLocationError.malformedURL)) // Should not happen. + } + + let request = UnauthenticatedRequest(request: .init(url: url)).asURLRequest() + let mapper = IPCountryCodeMapper() + enqueue(request, mapper: mapper, completion: onCompletion) + } +} + +/// `IPLocationRemote` known errors +/// +public extension IPLocationRemote { + enum IPLocationError: Error { + case malformedURL + } +} + +/// Private mapper used to extract the country code from the `IPLocationRemote` response. +/// +private struct IPCountryCodeMapper: Mapper { + + /// Response envelope + /// + struct Response: Decodable { + enum CodingKeys: String, CodingKey { + case countryCode = "country_short" + } + + let countryCode: String + } + + func map(response: Data) throws -> String { + try JSONDecoder().decode(Response.self, from: response).countryCode + } +} diff --git a/Networking/Networking/Requests/UnauthenticatedRequest.swift b/Networking/Networking/Requests/UnauthenticatedRequest.swift index 6995a27134e..1fd5a908268 100644 --- a/Networking/Networking/Requests/UnauthenticatedRequest.swift +++ b/Networking/Networking/Requests/UnauthenticatedRequest.swift @@ -3,7 +3,7 @@ import protocol Alamofire.URLRequestConvertible /// Wraps up a `URLRequestConvertible` instance, and injects the `UserAgent.defaultUserAgent`. /// -struct UnauthenticatedRequest: URLRequestConvertible { +struct UnauthenticatedRequest: Request { /// Request that does not require WPCOM authentication. /// @@ -19,4 +19,8 @@ struct UnauthenticatedRequest: URLRequestConvertible { return unauthenticated } + + func responseDataValidator() -> ResponseDataValidator { + PlaceholderDataValidator() + } } diff --git a/Networking/NetworkingTests/Remote/IPLocationRemoteTests.swift b/Networking/NetworkingTests/Remote/IPLocationRemoteTests.swift new file mode 100644 index 00000000000..528532e1e73 --- /dev/null +++ b/Networking/NetworkingTests/Remote/IPLocationRemoteTests.swift @@ -0,0 +1,33 @@ +import XCTest +import TestKit +@testable import Networking + + +final class IPLocationRemoteTests: XCTestCase { + /// Dummy Network Wrapper + /// + private var network: MockNetwork! + + override func setUp() { + super.setUp() + network = MockNetwork() + } + + func test_country_code_is_correctly_parsed() { + // Given + let remote = IPLocationRemote(network: network) + network.simulateResponse(requestUrlSuffix: "geo/", filename: "ip-location") + + // When + let countryCode = waitFor { promise in + remote.getIPCountryCode { result in + if case let .success(code) = result { + promise(code) + } + } + } + + // Then + XCTAssertEqual(countryCode, "CO") + } +} diff --git a/Networking/NetworkingTests/Responses/ip-location.json b/Networking/NetworkingTests/Responses/ip-location.json new file mode 100644 index 00000000000..672c6906712 --- /dev/null +++ b/Networking/NetworkingTests/Responses/ip-location.json @@ -0,0 +1,8 @@ +{ + "latitude": "25.77427", + "longitude": "-80.1936", + "country_short": "CO", + "country_long": "United States of America", + "region": "Florida", + "city": "Miami" +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Privacy/PrivacyBannerPresentationUseCase.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Privacy/PrivacyBannerPresentationUseCase.swift index 8075156b7b8..498a187bb2f 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Privacy/PrivacyBannerPresentationUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Privacy/PrivacyBannerPresentationUseCase.swift @@ -28,11 +28,15 @@ final class PrivacyBannerPresentationUseCase { /// Currently it is shown if the user is in the EU zone & privacy choices have not been saved. /// func shouldShowPrivacyBanner() async -> Bool { + // Early exit if privacy settings have been saved to prevent unnecessary API calls. + guard !defaults.hasSavedPrivacyBannerSettings else { + return false + } + do { let countryCode = try await fetchUsersCountryCode() let isCountryInEU = Country.GDPRCountryCodes.contains(countryCode) - let hasSavedPrivacySettings = defaults.hasSavedPrivacyBannerSettings - return isCountryInEU && !hasSavedPrivacySettings + return isCountryInEU } catch { DDLogInfo("⛔️ Could not determine users country code. Error: \(error)") return false @@ -43,41 +47,18 @@ final class PrivacyBannerPresentationUseCase { // MARK: Private Helpers private extension PrivacyBannerPresentationUseCase { - /// Determines the user country code by the following algorithm. - /// - If the user has a WPCOM account: - /// - Use the ip country code. - /// - If the user does not has a WPCOM account: - /// - Use the current locale country code. + /// Determines the user country. Relies on the public WordPress API. /// func fetchUsersCountryCode() async throws -> String { - // Use ip country code for WPCom accounts - if !stores.isAuthenticatedWithoutWPCom { - return try await fetchIPCountryCode() - } - - // Use locale country code as a fallback - return fetchLocaleCountryCode() - } - - /// Fetches the ip country code using the Account API. - /// - func fetchIPCountryCode() async throws -> String { try await withCheckedThrowingContinuation { continuation in - let action = AccountAction.synchronizeAccount { result in - let ipCountryCodeResult = result.map { $0.ipCountryCode } - continuation.resume(with: ipCountryCodeResult) + let action = UserAction.fetchUserIPCountryCode { result in + continuation.resume(with: result) } Task { @MainActor in stores.dispatch(action) } } } - - /// Fetches the country code from the current locate. - /// - func fetchLocaleCountryCode() -> String { - currentLocale.regionCode ?? "" - } } private extension UserDefaults { diff --git a/WooCommerce/WooCommerceTests/Internal/SessionManager+Internal.swift b/WooCommerce/WooCommerceTests/Internal/SessionManager+Internal.swift index d4bfbb3c456..6c969f90bd1 100644 --- a/WooCommerce/WooCommerceTests/Internal/SessionManager+Internal.swift +++ b/WooCommerce/WooCommerceTests/Internal/SessionManager+Internal.swift @@ -35,8 +35,7 @@ extension SessionManager { displayName: displayName, email: "", username: credentials.username, - gravatarUrl: nil, - ipCountryCode: "US") + gravatarUrl: nil) } return manager } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/AdminRoleRequiredViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/AdminRoleRequiredViewModelTests.swift index 60879d9811d..f3dae4d562c 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/AdminRoleRequiredViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/AdminRoleRequiredViewModelTests.swift @@ -42,6 +42,8 @@ final class AdminRoleRequiredViewModelTests: XCTestCase { case .retrieveUser(_, let onCompletion): let user = User.fake().copy(roles: [User.Role.administrator.rawValue]) onCompletion(.success(user)) + default: + break } } let viewModel = AdminRoleRequiredViewModel(siteID: 123, stores: stores) @@ -61,6 +63,8 @@ final class AdminRoleRequiredViewModelTests: XCTestCase { case .retrieveUser(_, let onCompletion): let user = User.fake().copy(roles: [User.Role.shopManager.rawValue]) onCompletion(.success(user)) + default: + break } } let viewModel = AdminRoleRequiredViewModel(siteID: 123, stores: stores) @@ -80,6 +84,8 @@ final class AdminRoleRequiredViewModelTests: XCTestCase { switch action { case .retrieveUser(_, let onCompletion): onCompletion(.failure(expectedError)) + default: + break } } let viewModel = AdminRoleRequiredViewModel(siteID: 123, stores: stores) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift index 3d7c1fa4242..7272e0e61b1 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift @@ -104,7 +104,7 @@ final class JetpackSetupCoordinatorTests: XCTestCase { let coordinator = JetpackSetupCoordinator(site: testSite, dotcomAuthScheme: expectedScheme, rootViewController: navigationController, stores: stores) let url = try XCTUnwrap(URL(string: "scheme://magic-login?token=test")) - let expectedAccount = Account(userID: 123, displayName: "Test", email: "test@example.com", username: "test", gravatarUrl: nil, ipCountryCode: "US") + let expectedAccount = Account(userID: 123, displayName: "Test", email: "test@example.com", username: "test", gravatarUrl: nil) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case let .loadWPComAccount(_, onCompletion): @@ -133,7 +133,7 @@ final class JetpackSetupCoordinatorTests: XCTestCase { let coordinator = JetpackSetupCoordinator(site: testSite, dotcomAuthScheme: expectedScheme, rootViewController: navigationController, stores: stores) let url = try XCTUnwrap(URL(string: "scheme://magic-login?token=test")) - let expectedAccount = Account(userID: 123, displayName: "Test", email: "test@example.com", username: "test", gravatarUrl: nil, ipCountryCode: "US") + let expectedAccount = Account(userID: 123, displayName: "Test", email: "test@example.com", username: "test", gravatarUrl: nil) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case let .loadWPComAccount(_, onCompletion): @@ -162,7 +162,7 @@ final class JetpackSetupCoordinatorTests: XCTestCase { let coordinator = JetpackSetupCoordinator(site: testSite, dotcomAuthScheme: expectedScheme, rootViewController: navigationController, stores: stores) let url = try XCTUnwrap(URL(string: "scheme://magic-login?token=test")) - let expectedAccount = Account(userID: 123, displayName: "Test", email: "test@example.com", username: "test", gravatarUrl: nil, ipCountryCode: "US") + let expectedAccount = Account(userID: 123, displayName: "Test", email: "test@example.com", username: "test", gravatarUrl: nil) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case let .loadWPComAccount(_, onCompletion): diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/Privacy/PrivacyBannerPresentationUseCaseTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/Privacy/PrivacyBannerPresentationUseCaseTests.swift index 4393a7827b7..11b129c963c 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/Privacy/PrivacyBannerPresentationUseCaseTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/Privacy/PrivacyBannerPresentationUseCaseTests.swift @@ -6,7 +6,7 @@ import TestKit final class PrivacyBannerPresentationUseCaseTests: XCTestCase { - @MainActor func test_show_banner_is_true_when_WPCOM_user_is_in_EU_and_choices_havent_been_saved() async throws { + @MainActor func test_show_banner_is_true_when_user_is_in_EU_and_choices_havent_been_saved() async throws { // Given let defaults = try XCTUnwrap(UserDefaults(suiteName: "TestingSuite")) defaults[.hasSavedPrivacyBannerSettings] = false @@ -14,11 +14,10 @@ final class PrivacyBannerPresentationUseCaseTests: XCTestCase { // Iterate through all of the country codes let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: true)) for euCode in Country.GDPRCountryCodes { - stores.whenReceivingAction(ofType: AccountAction.self) { action in + stores.whenReceivingAction(ofType: UserAction.self) { action in switch action { - case .synchronizeAccount(let onCompletion): - let account = Account(userID: 123, displayName: "", email: "", username: "", gravatarUrl: "", ipCountryCode: euCode) - onCompletion(.success(account)) + case .fetchUserIPCountryCode(let onCompletion): + onCompletion(.success(euCode)) default: break } @@ -33,17 +32,16 @@ final class PrivacyBannerPresentationUseCaseTests: XCTestCase { } } - @MainActor func test_show_banner_is_false_when_WPCOM_user_is_outside_of_EU_and_choices_have_not_been_saved() async throws { + @MainActor func test_show_banner_is_false_when_user_is_outside_of_EU_and_choices_have_not_been_saved() async throws { // Given let defaults = try XCTUnwrap(UserDefaults(suiteName: "TestingSuite")) defaults[.hasSavedPrivacyBannerSettings] = false let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: true)) - stores.whenReceivingAction(ofType: AccountAction.self) { action in + stores.whenReceivingAction(ofType: UserAction.self) { action in switch action { - case .synchronizeAccount(let onCompletion): - let account = Account(userID: 123, displayName: "", email: "", username: "", gravatarUrl: "", ipCountryCode: "US") - onCompletion(.success(account)) + case .fetchUserIPCountryCode(let onCompletion): + onCompletion(.success("US")) default: break } @@ -57,77 +55,17 @@ final class PrivacyBannerPresentationUseCaseTests: XCTestCase { XCTAssertFalse(shouldShowBanner) } - @MainActor func test_show_banner_is_false_when_WPCOM_user_is_inside_of_EU_and_choices_have_been_saved() async throws { + @MainActor func test_show_banner_is_false_when_choices_have_been_saved() async throws { // Given - let defaults = try XCTUnwrap(UserDefaults(suiteName: "TestingSuite")) - defaults[.hasSavedPrivacyBannerSettings] = true - let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: true)) - stores.whenReceivingAction(ofType: AccountAction.self) { action in - switch action { - case .synchronizeAccount(let onCompletion): - let account = Account(userID: 123, displayName: "", email: "", username: "", gravatarUrl: "", ipCountryCode: "GB") - onCompletion(.success(account)) - default: - break - } - } - - // When - let useCase = PrivacyBannerPresentationUseCase(defaults: defaults, stores: stores) - let shouldShowBanner = await useCase.shouldShowPrivacyBanner() - - // Then - XCTAssertFalse(shouldShowBanner) - } - - @MainActor func test_show_banner_is_true_when_non_WPCOM_user_has_EU_locale_and_choices_have_not_been_saved() async throws { - // Given - let defaults = try XCTUnwrap(UserDefaults(suiteName: "TestingSuite")) - defaults[.hasSavedPrivacyBannerSettings] = false - - let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: false)) - - // When - let useCase = PrivacyBannerPresentationUseCase(defaults: defaults, stores: stores, currentLocale: .init(identifier: "en_GB")) - let shouldShowBanner = await useCase.shouldShowPrivacyBanner() - - // Then - XCTAssertTrue(shouldShowBanner) - } - - @MainActor func test_show_banner_is_false_when_non_WPCOM_user_has_none_EU_locale_and_choices_have_not_been_saved() async throws { - // Given - let defaults = try XCTUnwrap(UserDefaults(suiteName: "TestingSuite")) - defaults[.hasSavedPrivacyBannerSettings] = false - - let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: false)) - - // When - let useCase = PrivacyBannerPresentationUseCase(defaults: defaults, stores: stores, currentLocale: .init(identifier: "en_US")) - let shouldShowBanner = await useCase.shouldShowPrivacyBanner() - - // Then - XCTAssertFalse(shouldShowBanner) - } - - @MainActor func test_show_banner_is_false_when_non_WPCOM_user_has_EU_locale_and_choices_have_been_saved() async throws { - // Given let defaults = try XCTUnwrap(UserDefaults(suiteName: "TestingSuite")) defaults[.hasSavedPrivacyBannerSettings] = true - let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: false)) - // When - let useCase = PrivacyBannerPresentationUseCase(defaults: defaults, stores: stores, currentLocale: .init(identifier: "en_GB")) + let useCase = PrivacyBannerPresentationUseCase(defaults: defaults, stores: stores) let shouldShowBanner = await useCase.shouldShowPrivacyBanner() // Then XCTAssertFalse(shouldShowBanner) } - - override class func tearDown() { - super.tearDown() - SessionManager.removeTestingDatabase() - } } diff --git a/Yosemite/Yosemite/Actions/UserAction.swift b/Yosemite/Yosemite/Actions/UserAction.swift index f251c090857..22a5359e020 100644 --- a/Yosemite/Yosemite/Actions/UserAction.swift +++ b/Yosemite/Yosemite/Actions/UserAction.swift @@ -10,4 +10,8 @@ public enum UserAction: Action { /// sites, it *must* be connected to dotcom via Jetpack. /// case retrieveUser(siteID: Int64, onCompletion: (Result) -> Void) + + /// Fetches the user IP's country code. Uses the WordPress public API.. + /// + case fetchUserIPCountryCode(onCompletion: (Result) -> Void) } diff --git a/Yosemite/Yosemite/Model/Mocks/ActionHandlers/MockUserActionHandler.swift b/Yosemite/Yosemite/Model/Mocks/ActionHandlers/MockUserActionHandler.swift index 9cd1c2ef5ca..8bcb48f213c 100644 --- a/Yosemite/Yosemite/Model/Mocks/ActionHandlers/MockUserActionHandler.swift +++ b/Yosemite/Yosemite/Model/Mocks/ActionHandlers/MockUserActionHandler.swift @@ -18,6 +18,8 @@ struct MockUserActionHandler: MockActionHandler { lastName: "", nickname: "", roles: [User.Role.administrator.rawValue]))) + case .fetchUserIPCountryCode(let onCompletion): + onCompletion(.success("CO")) } } } diff --git a/Yosemite/Yosemite/Model/Mocks/Graphs/ScreenshotsObjectGraph.swift b/Yosemite/Yosemite/Model/Mocks/Graphs/ScreenshotsObjectGraph.swift index f83c34b8f9c..296d676322c 100644 --- a/Yosemite/Yosemite/Model/Mocks/Graphs/ScreenshotsObjectGraph.swift +++ b/Yosemite/Yosemite/Model/Mocks/Graphs/ScreenshotsObjectGraph.swift @@ -29,8 +29,7 @@ struct ScreenshotObjectGraph: MockObjectGraph { displayName: i18n.DefaultAccount.displayName, email: i18n.DefaultAccount.email, username: i18n.DefaultAccount.username, - gravatarUrl: nil, - ipCountryCode: "USA" + gravatarUrl: nil ) let defaultSite = Site( diff --git a/Yosemite/Yosemite/Model/Storage/Account+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/Account+ReadOnlyConvertible.swift index 09e159b18a3..0f7174f10cd 100644 --- a/Yosemite/Yosemite/Model/Storage/Account+ReadOnlyConvertible.swift +++ b/Yosemite/Yosemite/Model/Storage/Account+ReadOnlyConvertible.swift @@ -23,7 +23,6 @@ extension Storage.Account: ReadOnlyConvertible { displayName: displayName ?? "", email: email ?? "", username: username ?? "", - gravatarUrl: gravatarUrl, - ipCountryCode: "") + gravatarUrl: gravatarUrl) } } diff --git a/Yosemite/Yosemite/Stores/UserStore.swift b/Yosemite/Yosemite/Stores/UserStore.swift index 216c7ee9080..a5a3a34f07a 100644 --- a/Yosemite/Yosemite/Stores/UserStore.swift +++ b/Yosemite/Yosemite/Stores/UserStore.swift @@ -6,9 +6,11 @@ import Storage // public final class UserStore: Store { private let remote: UserRemote + private let ipRemote: IPLocationRemote public override init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) { self.remote = UserRemote(network: network) + self.ipRemote = IPLocationRemote(network: network) super.init(dispatcher: dispatcher, storageManager: storageManager, network: network) } @@ -29,6 +31,8 @@ public final class UserStore: Store { switch action { case .retrieveUser(let siteID, let onCompletion): retrieveUser(siteID: siteID, completionHandler: onCompletion) + case .fetchUserIPCountryCode(let onCompletion): + fetchUserIPCountryCode(onCompletion: onCompletion) } } } @@ -39,4 +43,8 @@ private extension UserStore { func retrieveUser(siteID: Int64, completionHandler: @escaping (Result) -> Void) { remote.loadUserInfo(for: siteID, completion: completionHandler) } + + func fetchUserIPCountryCode(onCompletion: @escaping (Result) -> Void) { + ipRemote.getIPCountryCode(onCompletion: onCompletion) + } } diff --git a/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift b/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift index 1187eb15502..aae218ed036 100644 --- a/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift @@ -878,8 +878,7 @@ private extension AccountStoreTests { displayName: "Sample", email: "email@email.com", username: "Username!", - gravatarUrl: "https://automattic.com/superawesomegravatar.png", - ipCountryCode: "") + gravatarUrl: "https://automattic.com/superawesomegravatar.png") } /// Sample Account: Mark II @@ -889,8 +888,7 @@ private extension AccountStoreTests { displayName: "Yosemite", email: "yosemite@yosemite.com", username: "YOLO", - gravatarUrl: "https://automattic.com/yosemite.png", - ipCountryCode: "") + gravatarUrl: "https://automattic.com/yosemite.png") } func sampleAccountSettings() -> Networking.AccountSettings { diff --git a/Yosemite/YosemiteTests/Stores/UserStoreTests.swift b/Yosemite/YosemiteTests/Stores/UserStoreTests.swift index 28ab7477ae5..3373300e81d 100644 --- a/Yosemite/YosemiteTests/Stores/UserStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/UserStoreTests.swift @@ -69,4 +69,38 @@ final class UserStoreTests: XCTestCase { // Then XCTAssertTrue(result.isFailure) } + + func test_get_ip_location_returns_a_success() { + // Given + let urlSuffix = "geo/" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "ip-location") + let store = UserStore(dispatcher: dispatcher, storageManager: storageManager, network: network) + + // When + let result: Result = waitFor { promise in + let action = UserAction.fetchUserIPCountryCode() { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + } + + func test_get_ip_location_returns_error() { + // Given + let store = UserStore(dispatcher: dispatcher, storageManager: storageManager, network: network) + + // When + let result: Result = waitFor { promise in + let action = UserAction.fetchUserIPCountryCode() { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isFailure) + } }