Skip to content

Commit 49e9be2

Browse files
authored
Merge pull request #9851 from woocommerce/issue/9845-ip-location
[Privacy Choices] Use a consolidated API to fetch the user ip country code
2 parents f848462 + 37cbe5f commit 49e9be2

File tree

19 files changed

+187
-126
lines changed

19 files changed

+187
-126
lines changed

Fakes/Fakes/Networking.generated.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ extension Networking.Account {
2727
displayName: .fake(),
2828
email: .fake(),
2929
username: .fake(),
30-
gravatarUrl: .fake(),
31-
ipCountryCode: .fake()
30+
gravatarUrl: .fake()
3231
)
3332
}
3433
}

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@
177177
261CF1BC255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */; };
178178
261CF2CB255C50010090D8D3 /* payment-gateway-list-half.json in Resources */ = {isa = PBXBuildFile; fileRef = 261CF2CA255C50010090D8D3 /* payment-gateway-list-half.json */; };
179179
262E5AD5255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262E5AD4255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift */; };
180+
263659DC2A264A3E00607A0D /* IPLocationRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263659DB2A264A3E00607A0D /* IPLocationRemote.swift */; };
181+
263659DE2A2694A000607A0D /* IPLocationRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263659DD2A2694A000607A0D /* IPLocationRemoteTests.swift */; };
180182
263E37D22641ACEA00260D3B /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 263E37D12641ACEA00260D3B /* Codegen */; };
181183
263E383F2641FF1600260D3B /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 263E383E2641FF1600260D3B /* Codegen */; };
182184
263E38402641FF1600260D3B /* Codegen in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 263E383E2641FF1600260D3B /* Codegen */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
@@ -211,6 +213,7 @@
211213
268B68FB24C87384007EBF1D /* leaderboards-products.json in Resources */ = {isa = PBXBuildFile; fileRef = 268B68FA24C87384007EBF1D /* leaderboards-products.json */; };
212214
268B68FD24C87E37007EBF1D /* leaderboards-year-alt.json in Resources */ = {isa = PBXBuildFile; fileRef = 268B68FC24C87E37007EBF1D /* leaderboards-year-alt.json */; };
213215
268EC45C26C169F600716F5C /* order-with-faulty-attributes.json in Resources */ = {isa = PBXBuildFile; fileRef = 268EC45B26C169F600716F5C /* order-with-faulty-attributes.json */; };
216+
26B15E442A269F79000C35E4 /* ip-location.json in Resources */ = {isa = PBXBuildFile; fileRef = 26B15E432A269F79000C35E4 /* ip-location.json */; };
214217
26B2F74124C1F2C10065CCC8 /* LeaderboardsRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B2F74024C1F2C10065CCC8 /* LeaderboardsRemote.swift */; };
215218
26B2F74324C545D50065CCC8 /* Leaderboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B2F74224C545D50065CCC8 /* Leaderboard.swift */; };
216219
26B2F74524C5573F0065CCC8 /* LeaderboardListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B2F74424C5573F0065CCC8 /* LeaderboardListMapper.swift */; };
@@ -1111,6 +1114,8 @@
11111114
261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsGatewayRemoteTests.swift; sourceTree = "<group>"; };
11121115
261CF2CA255C50010090D8D3 /* payment-gateway-list-half.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "payment-gateway-list-half.json"; sourceTree = "<group>"; };
11131116
262E5AD4255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayListMapperTests.swift; sourceTree = "<group>"; };
1117+
263659DB2A264A3E00607A0D /* IPLocationRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemote.swift; sourceTree = "<group>"; };
1118+
263659DD2A2694A000607A0D /* IPLocationRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemoteTests.swift; sourceTree = "<group>"; };
11141119
265BCA01243056E3004E53EE /* categories-all.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "categories-all.json"; sourceTree = "<group>"; };
11151120
265EFBDB285257950033BD33 /* Order+Fallbacks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Order+Fallbacks.swift"; sourceTree = "<group>"; };
11161121
26615472242D596B00A31661 /* ProductCategoriesRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCategoriesRemote.swift; sourceTree = "<group>"; };
@@ -1138,6 +1143,7 @@
11381143
268B68FA24C87384007EBF1D /* leaderboards-products.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "leaderboards-products.json"; sourceTree = "<group>"; };
11391144
268B68FC24C87E37007EBF1D /* leaderboards-year-alt.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "leaderboards-year-alt.json"; sourceTree = "<group>"; };
11401145
268EC45B26C169F600716F5C /* order-with-faulty-attributes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-with-faulty-attributes.json"; sourceTree = "<group>"; };
1146+
26B15E432A269F79000C35E4 /* ip-location.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "ip-location.json"; sourceTree = "<group>"; };
11411147
26B2F74024C1F2C10065CCC8 /* LeaderboardsRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardsRemote.swift; sourceTree = "<group>"; };
11421148
26B2F74224C545D50065CCC8 /* Leaderboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Leaderboard.swift; sourceTree = "<group>"; };
11431149
26B2F74424C5573F0065CCC8 /* LeaderboardListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardListMapper.swift; sourceTree = "<group>"; };
@@ -2076,6 +2082,7 @@
20762082
24F98C5F2502EF8200F49B68 /* FeatureFlagRemoteTests.swift */,
20772083
4513382127A8409000AE5E78 /* InboxNotesRemoteTests.swift */,
20782084
E13BAD5228F8625600217769 /* InAppPurchasesRemoteTests.swift */,
2085+
263659DD2A2694A000607A0D /* IPLocationRemoteTests.swift */,
20792086
03EB99892906AB0C00F06A39 /* JustInTimeMessagesRemoteTests.swift */,
20802087
26B2F74824C55ACE0065CCC8 /* LeaderboardsRemoteTests.swift */,
20812088
020D07BF23D8587700FD9580 /* MediaRemoteTests.swift */,
@@ -2230,6 +2237,7 @@
22302237
24F98C512502E79800F49B68 /* FeatureFlagRemote.swift */,
22312238
E18152BD28F85B5B0011A0EC /* InAppPurchasesRemote.swift */,
22322239
4513381F27A8227F00AE5E78 /* InboxNotesRemote.swift */,
2240+
263659DB2A264A3E00607A0D /* IPLocationRemote.swift */,
22332241
03EB99872906A78400F06A39 /* JustInTimeMessagesRemote.swift */,
22342242
26B2F74024C1F2C10065CCC8 /* LeaderboardsRemote.swift */,
22352243
B5DAEFEF2180DD5A0002356A /* NotificationsRemote.swift */,
@@ -2459,6 +2467,7 @@
24592467
DE50296228C609DE00551736 /* jetpack-user-not-connected.json */,
24602468
02B41A91296BEB3000FE3311 /* load-site-current-plan-success.json */,
24612469
02B41A93296C04BC00FE3311 /* load-site-plans-no-current-plan.json */,
2470+
26B15E432A269F79000C35E4 /* ip-location.json */,
24622471
EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */,
24632472
02B41A8F296BC85800FE3311 /* site-domains.json */,
24642473
02935AED29DFFA74001B793E /* site-enable-trial-error-already-upgraded.json */,
@@ -3346,6 +3355,7 @@
33463355
026CF624237D839B009563D4 /* product-variations-load-all.json in Resources */,
33473356
02AF07EC27492FDD00B2D81E /* media-library-from-wordpress-site.json in Resources */,
33483357
CC9A253C26442C71005DE56E /* shipping-label-eligibility-success.json in Resources */,
3358+
26B15E442A269F79000C35E4 /* ip-location.json in Resources */,
33493359
B5A24179217F98F600595DEF /* notifications-load-all.json in Resources */,
33503360
DEA6B1C9296D0E8B005AA5E9 /* systemStatusWithPluginsOnly-without-data.json in Resources */,
33513361
02C4325F298A55D100F14AEE /* domain-contact-info.json in Resources */,
@@ -3770,6 +3780,7 @@
37703780
09EA564B27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift in Sources */,
37713781
CE227093228DD44C00C0626C /* ProductStatus.swift in Sources */,
37723782
451A97E9260B657D0059D135 /* ShippingLabelPredefinedOption.swift in Sources */,
3783+
263659DC2A264A3E00607A0D /* IPLocationRemote.swift in Sources */,
37733784
02C2548425635BD000A04423 /* ShippingLabelPaperSize.swift in Sources */,
37743785
CE132BBC223859710029DB6C /* ProductTag.swift in Sources */,
37753786
DE66C5532976508300DAA978 /* CookieNonceAuthenticator.swift in Sources */,
@@ -4241,6 +4252,7 @@
42414252
D8FBFF0F22D3B25E006E3336 /* WooAPIVersionTests.swift in Sources */,
42424253
45152831257A8E1A0076B03C /* ProductAttributeMapperTests.swift in Sources */,
42434254
CCA1D60A2943809700B40560 /* SiteSummaryStatsMapperTests.swift in Sources */,
4255+
263659DE2A2694A000607A0D /* IPLocationRemoteTests.swift in Sources */,
42444256
26B2F74924C55ACE0065CCC8 /* LeaderboardsRemoteTests.swift in Sources */,
42454257
45CCFCE827A2E5020012E8CB /* InboxNoteListMapperTests.swift in Sources */,
42464258
74002D6C2118B88200A63C19 /* SiteStatsRemoteTests.swift in Sources */,

Networking/Networking/Model/Account.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,14 @@ public struct Account: Decodable, Equatable, GeneratedFakeable {
2525
///
2626
public let gravatarUrl: String?
2727

28-
/// Users IP country Code
29-
/// This setting is not stored in the Storage layer because we don't want to rely on stale value.
30-
/// But there us no problem on add it later if we believe it will be useful.
31-
///
32-
public let ipCountryCode: String
33-
34-
3528
/// Designated Initializer.
3629
///
37-
public init(userID: Int64, displayName: String, email: String, username: String, gravatarUrl: String?, ipCountryCode: String) {
30+
public init(userID: Int64, displayName: String, email: String, username: String, gravatarUrl: String?) {
3831
self.userID = userID
3932
self.displayName = displayName
4033
self.email = email
4134
self.username = username
4235
self.gravatarUrl = gravatarUrl
43-
self.ipCountryCode = ipCountryCode
4436
}
4537
}
4638

@@ -55,6 +47,5 @@ private extension Account {
5547
case email = "email"
5648
case username = "username"
5749
case gravatarUrl = "avatar_URL"
58-
case ipCountryCode = "user_ip_country_code"
5950
}
6051
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
3+
/// Remote type to fetch the user's IP Location using the public `geo` API.
4+
///
5+
public final class IPLocationRemote: Remote {
6+
7+
/// Fetches the country code from the device ip.
8+
///
9+
public func getIPCountryCode(onCompletion: @escaping (Result<String, Error>) -> Void) {
10+
let path = "geo/" // Needs the trailing slash otherwise the request will fail.
11+
guard let url = URL(string: Settings.wordpressApiBaseURL + path) else {
12+
return onCompletion(.failure(IPLocationError.malformedURL)) // Should not happen.
13+
}
14+
15+
let request = UnauthenticatedRequest(request: .init(url: url)).asURLRequest()
16+
let mapper = IPCountryCodeMapper()
17+
enqueue(request, mapper: mapper, completion: onCompletion)
18+
}
19+
}
20+
21+
/// `IPLocationRemote` known errors
22+
///
23+
public extension IPLocationRemote {
24+
enum IPLocationError: Error {
25+
case malformedURL
26+
}
27+
}
28+
29+
/// Private mapper used to extract the country code from the `IPLocationRemote` response.
30+
///
31+
private struct IPCountryCodeMapper: Mapper {
32+
33+
/// Response envelope
34+
///
35+
struct Response: Decodable {
36+
enum CodingKeys: String, CodingKey {
37+
case countryCode = "country_short"
38+
}
39+
40+
let countryCode: String
41+
}
42+
43+
func map(response: Data) throws -> String {
44+
try JSONDecoder().decode(Response.self, from: response).countryCode
45+
}
46+
}

Networking/Networking/Requests/UnauthenticatedRequest.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import protocol Alamofire.URLRequestConvertible
33

44
/// Wraps up a `URLRequestConvertible` instance, and injects the `UserAgent.defaultUserAgent`.
55
///
6-
struct UnauthenticatedRequest: URLRequestConvertible {
6+
struct UnauthenticatedRequest: Request {
77

88
/// Request that does not require WPCOM authentication.
99
///
@@ -19,4 +19,8 @@ struct UnauthenticatedRequest: URLRequestConvertible {
1919

2020
return unauthenticated
2121
}
22+
23+
func responseDataValidator() -> ResponseDataValidator {
24+
PlaceholderDataValidator()
25+
}
2226
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import XCTest
2+
import TestKit
3+
@testable import Networking
4+
5+
6+
final class IPLocationRemoteTests: XCTestCase {
7+
/// Dummy Network Wrapper
8+
///
9+
private var network: MockNetwork!
10+
11+
override func setUp() {
12+
super.setUp()
13+
network = MockNetwork()
14+
}
15+
16+
func test_country_code_is_correctly_parsed() {
17+
// Given
18+
let remote = IPLocationRemote(network: network)
19+
network.simulateResponse(requestUrlSuffix: "geo/", filename: "ip-location")
20+
21+
// When
22+
let countryCode = waitFor { promise in
23+
remote.getIPCountryCode { result in
24+
if case let .success(code) = result {
25+
promise(code)
26+
}
27+
}
28+
}
29+
30+
// Then
31+
XCTAssertEqual(countryCode, "CO")
32+
}
33+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"latitude": "25.77427",
3+
"longitude": "-80.1936",
4+
"country_short": "CO",
5+
"country_long": "United States of America",
6+
"region": "Florida",
7+
"city": "Miami"
8+
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Privacy/PrivacyBannerPresentationUseCase.swift

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ final class PrivacyBannerPresentationUseCase {
2828
/// Currently it is shown if the user is in the EU zone & privacy choices have not been saved.
2929
///
3030
func shouldShowPrivacyBanner() async -> Bool {
31+
// Early exit if privacy settings have been saved to prevent unnecessary API calls.
32+
guard !defaults.hasSavedPrivacyBannerSettings else {
33+
return false
34+
}
35+
3136
do {
3237
let countryCode = try await fetchUsersCountryCode()
3338
let isCountryInEU = Country.GDPRCountryCodes.contains(countryCode)
34-
let hasSavedPrivacySettings = defaults.hasSavedPrivacyBannerSettings
35-
return isCountryInEU && !hasSavedPrivacySettings
39+
return isCountryInEU
3640
} catch {
3741
DDLogInfo("⛔️ Could not determine users country code. Error: \(error)")
3842
return false
@@ -43,41 +47,18 @@ final class PrivacyBannerPresentationUseCase {
4347
// MARK: Private Helpers
4448
private extension PrivacyBannerPresentationUseCase {
4549

46-
/// Determines the user country code by the following algorithm.
47-
/// - If the user has a WPCOM account:
48-
/// - Use the ip country code.
49-
/// - If the user does not has a WPCOM account:
50-
/// - Use the current locale country code.
50+
/// Determines the user country. Relies on the public WordPress API.
5151
///
5252
func fetchUsersCountryCode() async throws -> String {
53-
// Use ip country code for WPCom accounts
54-
if !stores.isAuthenticatedWithoutWPCom {
55-
return try await fetchIPCountryCode()
56-
}
57-
58-
// Use locale country code as a fallback
59-
return fetchLocaleCountryCode()
60-
}
61-
62-
/// Fetches the ip country code using the Account API.
63-
///
64-
func fetchIPCountryCode() async throws -> String {
6553
try await withCheckedThrowingContinuation { continuation in
66-
let action = AccountAction.synchronizeAccount { result in
67-
let ipCountryCodeResult = result.map { $0.ipCountryCode }
68-
continuation.resume(with: ipCountryCodeResult)
54+
let action = UserAction.fetchUserIPCountryCode { result in
55+
continuation.resume(with: result)
6956
}
7057
Task { @MainActor in
7158
stores.dispatch(action)
7259
}
7360
}
7461
}
75-
76-
/// Fetches the country code from the current locate.
77-
///
78-
func fetchLocaleCountryCode() -> String {
79-
currentLocale.regionCode ?? ""
80-
}
8162
}
8263

8364
private extension UserDefaults {

WooCommerce/WooCommerceTests/Internal/SessionManager+Internal.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ extension SessionManager {
3535
displayName: displayName,
3636
email: "",
3737
username: credentials.username,
38-
gravatarUrl: nil,
39-
ipCountryCode: "US")
38+
gravatarUrl: nil)
4039
}
4140
return manager
4241
}

WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/AdminRoleRequiredViewModelTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ final class AdminRoleRequiredViewModelTests: XCTestCase {
4242
case .retrieveUser(_, let onCompletion):
4343
let user = User.fake().copy(roles: [User.Role.administrator.rawValue])
4444
onCompletion(.success(user))
45+
default:
46+
break
4547
}
4648
}
4749
let viewModel = AdminRoleRequiredViewModel(siteID: 123, stores: stores)
@@ -61,6 +63,8 @@ final class AdminRoleRequiredViewModelTests: XCTestCase {
6163
case .retrieveUser(_, let onCompletion):
6264
let user = User.fake().copy(roles: [User.Role.shopManager.rawValue])
6365
onCompletion(.success(user))
66+
default:
67+
break
6468
}
6569
}
6670
let viewModel = AdminRoleRequiredViewModel(siteID: 123, stores: stores)
@@ -80,6 +84,8 @@ final class AdminRoleRequiredViewModelTests: XCTestCase {
8084
switch action {
8185
case .retrieveUser(_, let onCompletion):
8286
onCompletion(.failure(expectedError))
87+
default:
88+
break
8389
}
8490
}
8591
let viewModel = AdminRoleRequiredViewModel(siteID: 123, stores: stores)

0 commit comments

Comments
 (0)