Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit c104ac4

Browse files
authored
Merge pull request #613 from wordpress-mobile/feature/eu-us-compliance-locator
[EU-US Compliance] Add `IPLocationRemote`
2 parents e82a089 + 035a9d7 commit c104ac4

File tree

4 files changed

+155
-0
lines changed

4 files changed

+155
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ _None._
3838

3939
### New Features
4040

41+
- Add `IPLocationRemote` [#613]
4142
- Add `ui_status` field to `BlazeCampaign` [#611]
4243

4344
### Bug Fixes

WordPressKit.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/* Begin PBXBuildFile section */
1010
0152100C28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */; };
11+
0847B92C2A4442730044D32F /* IPLocationRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0847B92B2A4442730044D32F /* IPLocationRemote.swift */; };
12+
08C7493E2A45EA11000DA0E2 /* IPLocationRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */; };
1113
0CB1905E2A2A5E83004D3E80 /* BlazeCampaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */; };
1214
0CB190612A2A6A13004D3E80 /* blaze-campaigns-search.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */; };
1315
0CB190652A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */; };
@@ -687,6 +689,8 @@
687689

688690
/* Begin PBXFileReference section */
689691
0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAnnualAndMostPopularTimeInsightDecodingTests.swift; sourceTree = "<group>"; };
692+
0847B92B2A4442730044D32F /* IPLocationRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemote.swift; sourceTree = "<group>"; };
693+
08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPLocationRemoteTests.swift; sourceTree = "<group>"; };
690694
0C3A2A412A2E7BA500FD91D6 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
691695
0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaign.swift; sourceTree = "<group>"; };
692696
0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-campaigns-search.json"; sourceTree = "<group>"; };
@@ -1389,6 +1393,14 @@
13891393
/* End PBXFrameworksBuildPhase section */
13901394

13911395
/* Begin PBXGroup section */
1396+
08C7493C2A45E9E3000DA0E2 /* Privacy */ = {
1397+
isa = PBXGroup;
1398+
children = (
1399+
08C7493D2A45EA11000DA0E2 /* IPLocationRemoteTests.swift */,
1400+
);
1401+
name = Privacy;
1402+
sourceTree = "<group>";
1403+
};
13921404
0CB1905C2A2A5E7B004D3E80 /* Blaze */ = {
13931405
isa = PBXGroup;
13941406
children = (
@@ -1763,6 +1775,7 @@
17631775
7433BC061EFC456B002D9E92 /* Plans */,
17641776
E13EE14A1F332C3100C15787 /* Plugins */,
17651777
740B23D41F17F6D200067A2A /* Post */,
1778+
08C7493C2A45E9E3000DA0E2 /* Privacy */,
17661779
C738CAED286222C9001BE107 /* QR Login */,
17671780
7430C9BF1F192C210051B8E6 /* Reader */,
17681781
74E2294C1F1E73650085F7F2 /* Sharing */,
@@ -1883,6 +1896,7 @@
18831896
F9E56DF724EB125600916770 /* FeatureFlagRemote.swift */,
18841897
74650F711F0EA1A700188EDB /* GravatarServiceRemote.swift */,
18851898
1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */,
1899+
0847B92B2A4442730044D32F /* IPLocationRemote.swift */,
18861900
FAD1344425908F5F00A8FEB1 /* JetpackBackupServiceRemote.swift */,
18871901
8B749DEC25AF3E4600023F03 /* JetpackCapabilitiesServiceRemote.swift */,
18881902
32FC1D27255C91ED00CD0A7B /* JetpackScanServiceRemote.swift */,
@@ -3224,6 +3238,7 @@
32243238
9F4E52002088E38200424676 /* ObjectValidation.swift in Sources */,
32253239
7EC60EC022DC5D7C00FB0336 /* EditorSettings.swift in Sources */,
32263240
7403A3021EF0726E00DED7DC /* AccountSettings.swift in Sources */,
3241+
0847B92C2A4442730044D32F /* IPLocationRemote.swift in Sources */,
32273242
40E7FEA9220FA4060032834E /* StatsEmailFollowersInsight.swift in Sources */,
32283243
404057DA221C9D560060250C /* StatsTopReferrersTimeIntervalData.swift in Sources */,
32293244
826016F11F9FA13A00533B6C /* ActivityServiceRemote.swift in Sources */,
@@ -3384,6 +3399,7 @@
33843399
E13EE14C1F332C4400C15787 /* PluginServiceRemoteTests.swift in Sources */,
33853400
736C971021E80D48007A4200 /* SiteVerticalsPromptResponseDecodingTests.swift in Sources */,
33863401
74B335D81F06F1CA0053A184 /* MockWordPressComRestApi.swift in Sources */,
3402+
08C7493E2A45EA11000DA0E2 /* IPLocationRemoteTests.swift in Sources */,
33873403
32AF21E3236DEB3C001C6502 /* PostServiceRemoteRESTAutosaveTests.swift in Sources */,
33883404
3236F79A24AE406D0088E8F3 /* ReaderTopicServiceRemote+InterestsTests.swift in Sources */,
33893405
FEE4EF5B27302317003CDA3C /* CommentServiceRemoteREST+APIv2Tests.swift in Sources */,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 {
6+
private enum Constants {
7+
static let jsonDecoder = JSONDecoder()
8+
}
9+
10+
private let urlSession: URLSession
11+
12+
init(urlSession: URLSession = URLSession.shared) {
13+
self.urlSession = urlSession
14+
}
15+
16+
/// Fetches the country code from the device ip.
17+
///
18+
public func fetchIPCountryCode(completion: @escaping (Result<String, Error>) -> Void) {
19+
let path = WordPressComOAuthClient.WordPressComOAuthDefaultApiBaseUrl + "/geo/"
20+
guard let url = URL(string: path) else {
21+
return completion(.failure(IPLocationError.malformedURL))
22+
}
23+
24+
let request = URLRequest(url: url)
25+
let task = urlSession.dataTask(with: request) { data, _, error in
26+
guard let data else {
27+
completion(.failure(IPLocationError.requestFailure(error)))
28+
return
29+
}
30+
31+
do {
32+
let result = try Constants.jsonDecoder.decode(RemoteIPCountryCode.self, from: data)
33+
completion(.success(result.countryCode))
34+
} catch {
35+
completion(.failure(error))
36+
}
37+
}
38+
task.resume()
39+
}
40+
}
41+
42+
public extension IPLocationRemote {
43+
enum IPLocationError: Error {
44+
case malformedURL
45+
case requestFailure(Error?)
46+
}
47+
}
48+
49+
public struct RemoteIPCountryCode: Decodable {
50+
enum CodingKeys: String, CodingKey {
51+
case countryCode = "country_short"
52+
}
53+
54+
let countryCode: String
55+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import XCTest
2+
@testable import WordPressKit
3+
4+
5+
final class IPLocationRemoteTests: XCTestCase {
6+
var remote: IPLocationRemote!
7+
let apiURL = URL(string: "https://public-api.wordpress.com/geo/")!
8+
9+
override func setUp() {
10+
super.setUp()
11+
let configuration = URLSessionConfiguration.default
12+
configuration.protocolClasses = [MockURLProtocol.self]
13+
let urlSession = URLSession.init(configuration: configuration)
14+
15+
remote = IPLocationRemote(urlSession: urlSession)
16+
}
17+
18+
func testCountryCodeIsCorrectlyParsed() {
19+
let expectation = expectation(description: "The country code should be parsed as DE")
20+
let jsonString = """
21+
{
22+
"latitude": "24.917202",
23+
"longitude": "16.559613",
24+
"country_short": "DE",
25+
"country_long": "Germany",
26+
"region": "Brandenburg",
27+
"city": "Potsdam"
28+
}
29+
"""
30+
31+
let data = jsonString.data(using: .utf8)
32+
33+
MockURLProtocol.requestHandler = { request in
34+
let response = HTTPURLResponse(url: self.apiURL, statusCode: 200, httpVersion: nil, headerFields: nil)!
35+
return (response, data)
36+
}
37+
38+
remote.fetchIPCountryCode { result in
39+
switch result {
40+
case .success(let countryCode):
41+
XCTAssertEqual(countryCode, "DE")
42+
case .failure(let error):
43+
XCTFail("Error was not expected: \(error)")
44+
}
45+
expectation.fulfill()
46+
}
47+
wait(for: [expectation], timeout: 1.0)
48+
}
49+
}
50+
51+
final class MockURLProtocol: URLProtocol {
52+
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data?))?
53+
54+
override class func canInit(with request: URLRequest) -> Bool {
55+
return true
56+
}
57+
58+
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
59+
return request
60+
}
61+
62+
63+
override func startLoading() {
64+
guard let handler = MockURLProtocol.requestHandler else {
65+
fatalError("Handler is unavailable.")
66+
}
67+
68+
do {
69+
let (response, data) = try handler(request)
70+
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
71+
72+
if let data = data {
73+
client?.urlProtocol(self, didLoad: data)
74+
}
75+
client?.urlProtocolDidFinishLoading(self)
76+
} catch {
77+
client?.urlProtocol(self, didFailWithError: error)
78+
}
79+
}
80+
81+
82+
override func stopLoading() { }
83+
}

0 commit comments

Comments
 (0)