Skip to content

Commit 28befed

Browse files
authored
[MBL-2855][2/2] Bypass pledge manager decision policy (#2645)
* Refactor the pledge manager native navigation so all cases are represented in an enum * Add feature flag for bypassing the decision policy * Allow unrecognized urls to render when bypass flag is on * Add basic error handling
1 parent 5d83da3 commit 28befed

File tree

5 files changed

+96
-16
lines changed

5 files changed

+96
-16
lines changed

Library/RemoteConfig/RemoteConfigFeature+Helpers.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ func featureEnabled(feature: RemoteConfigFeature) -> Bool {
1515
return false
1616
}
1717

18+
public func featureBypassPledgeManagerDecisionPolicyEnabled() -> Bool {
19+
return featureEnabled(feature: .bypassPledgeManagerDecisionPolicy)
20+
}
21+
1822
public func featureEditPledgeOverTimeEnabled() -> Bool {
1923
return featureEnabled(feature: .editPledgeOverTimeEnabled)
2024
}

Library/RemoteConfig/RemoteConfigFeature.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22

33
public enum RemoteConfigFeature: String, CaseIterable {
4+
case bypassPledgeManagerDecisionPolicy = "bypass_pledge_manager_decision_policy"
45
case editPledgeOverTimeEnabled = "edit_pledge_over_time"
56
case useKeychainForOAuthToken = "use_keychain_for_oauth_token"
67
case pledgedProjectsOverviewV2Enabled = "pledged_projects_overview_v2"
@@ -12,6 +13,7 @@ public enum RemoteConfigFeature: String, CaseIterable {
1213
extension RemoteConfigFeature: CustomStringConvertible {
1314
public var description: String {
1415
switch self {
16+
case .bypassPledgeManagerDecisionPolicy: return "Bypass Pledge Manager Decision Policy"
1517
case .editPledgeOverTimeEnabled: return "Edit Pledge Over Time"
1618
case .useKeychainForOAuthToken: return "Use Keychain for OAuth token"
1719
case .pledgedProjectsOverviewV2Enabled: return "Pledged Projects Overview V2"

Library/ViewModels/PledgeManagerWebViewModel.swift

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import FirebaseCrashlytics
12
import KsApi
23
import Prelude
34
import ReactiveExtensions
@@ -145,13 +146,31 @@ public final class PledgeManagerWebViewModel: PledgeManagerWebViewModelType {
145146
return true
146147
}
147148

149+
// A supported request will be prepared by logic elsewhere in the class and loaded into
150+
// the web view from there. Never allow the unprepared version of these to render.
148151
let request = action.request
152+
if isSupportedRequest(request: request) {
153+
return AppEnvironment.current.apiService.isPrepared(request: request)
154+
}
149155

150-
if !AppEnvironment.current.apiService.isPrepared(request: request) {
156+
// If the corresponding native navigation request exists,
157+
// this request will be handled elsewhere.
158+
if nativeNavigationRequestForURLRequest(request) != nil {
151159
return false
152160
}
153161

154-
return isSupportedRequest(request: request)
162+
// Log unrecognized urls.
163+
if let error = errorForUnrecognizedUrl(request: request) {
164+
Crashlytics.crashlytics().record(error: error)
165+
}
166+
167+
// Never show unsupported kickstarter navigation requests, since these
168+
// can get the user into bad/weird states.
169+
if isKickstarterRequest(request) {
170+
return false
171+
}
172+
173+
return featureBypassPledgeManagerDecisionPolicyEnabled()
155174
}
156175
.map { $0 ? .allow : .cancel }
157176

@@ -218,6 +237,11 @@ private func nativeNavigationRequestForURLRequest(_ request: URLRequest)
218237
}
219238
}
220239

240+
private func isKickstarterRequest(_ request: URLRequest) -> Bool {
241+
guard let host = request.url?.host() else { return false }
242+
return host == AppEnvironment.current.apiService.serverConfig.webBaseUrl.host()
243+
}
244+
221245
private func isUnpreparedSupportedRequest(request: URLRequest) -> Bool {
222246
guard !AppEnvironment.current.apiService.isPrepared(request: request) else { return false }
223247
return isSupportedRequest(request: request)
@@ -247,3 +271,35 @@ private func isStripeHost(_ host: String) -> Bool {
247271
let withoutSubdomain = host.lowercased().split(separator: ".").suffix(2).joined(separator: ".")
248272
return stripeDomains.contains(withoutSubdomain)
249273
}
274+
275+
private func errorForUnrecognizedUrl(request: URLRequest) -> NSError? {
276+
let errorDomain = "Kickstarter.PledgeManagerWebView"
277+
enum ErrorCode: Int {
278+
case unhandledKickstarterUrl = 1
279+
case unrecognizedUrl = 2
280+
}
281+
282+
// If there's no url present, don't log an error. The "about:blank" happens
283+
// every time the web view loads, so logging these would be too noisy.
284+
guard let url = request.url, url.absoluteString != "about:blank" else {
285+
return nil
286+
}
287+
288+
if isKickstarterRequest(request) {
289+
return NSError(
290+
domain: errorDomain,
291+
code: ErrorCode.unhandledKickstarterUrl.rawValue,
292+
userInfo: [
293+
NSLocalizedDescriptionKey: "Found unhandled kickstarter url"
294+
]
295+
)
296+
}
297+
298+
return NSError(
299+
domain: errorDomain,
300+
code: ErrorCode.unrecognizedUrl.rawValue,
301+
userInfo: [
302+
NSLocalizedDescriptionKey: "Found unrecongnized url request with host: \(url.host() ?? "Unknown")"
303+
]
304+
)
305+
}

Library/ViewModels/PledgeManagerWebViewModelTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,21 @@ final class PledgeManagerWebViewModelTests: TestCase {
267267

268268
// MARK: - Decision policy tests
269269

270+
func testDecisionPolicyBypass() {
271+
let mockConfigClient = MockRemoteConfigClient()
272+
mockConfigClient.features = [
273+
RemoteConfigFeature.bypassPledgeManagerDecisionPolicy.rawValue: true
274+
]
275+
276+
withEnvironment(remoteConfigClient: mockConfigClient) {
277+
let navigationData = navigationData("https://www.fake.com/unrecognized-url")
278+
XCTAssertEqual(
279+
self.vm.decidePolicyFor(navigationAction: navigationData),
280+
WKNavigationActionPolicy.allow
281+
)
282+
}
283+
}
284+
270285
func testBadRequest() {
271286
let navigationData = navigationData("https://www.fake.com/bad-url")
272287
XCTAssertEqual(

Library/ViewModels/RemoteConfigFeatureFlagToolsViewModelTests.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,47 +60,50 @@ final class RemoteConfigFlagToolsViewModelTests: TestCase {
6060
}
6161
}
6262

63-
func testUpdateUserDefaultsWithFeature_FeatureIsEnabled() {
63+
func testUpdateUserDefaultsWithFeature_FeatureIsDisabledAndEnabled() {
6464
let mockRemoteConfigClient = MockRemoteConfigClient()
65-
|> \.features .~ [
66-
RemoteConfigFeature.editPledgeOverTimeEnabled.rawValue: false
67-
]
6865

6966
withEnvironment(remoteConfigClient: mockRemoteConfigClient) {
7067
self.vm.inputs.viewDidLoad()
71-
7268
self.scheduler.advance()
73-
7469
self.updateUserDefaultsWithFeatures.assertDidNotEmitValue()
7570

76-
self.vm.inputs.setFeatureAtIndexEnabled(index: 0, isEnabled: true)
77-
71+
self.vm.inputs.setFeatureAtIndexEnabled(index: 0, isEnabled: false)
7872
self.scheduler.advance()
73+
let (_, isEnabled1) = self.updateUserDefaultsWithFeatures.lastValue![0]
74+
XCTAssertFalse(isEnabled1)
7975

80-
let (_, isEnabled1) = self.updateUserDefaultsWithFeatures.values[0][0]
81-
82-
XCTAssertTrue(isEnabled1)
76+
self.vm.inputs.setFeatureAtIndexEnabled(index: 0, isEnabled: true)
77+
self.scheduler.advance()
78+
let (_, isEnabled2) = self.updateUserDefaultsWithFeatures.lastValue![0]
79+
XCTAssertTrue(isEnabled2)
8380
}
8481
}
8582

8683
func testUpdateUserDefaultsWithFeatures_ReloadWithData_UserDefaultsIsUpdated() {
84+
let feature = RemoteConfigFeature.editPledgeOverTimeEnabled
8785
let mockRemoteConfigClient = MockRemoteConfigClient()
8886
|> \.features .~ [
89-
RemoteConfigFeature.editPledgeOverTimeEnabled.rawValue: false
87+
feature.rawValue: false
9088
]
9189

9290
withEnvironment(remoteConfigClient: mockRemoteConfigClient, userDefaults: userDefaults) {
9391
self.vm.inputs.viewDidLoad()
9492

9593
self.scheduler.advance()
9694

95+
guard let index = (self.reloadWithData.lastValue?.firstIndex { $0.0 == feature }) else {
96+
XCTFail("Expected to find feature \(feature) in the list of features.")
97+
return
98+
}
99+
97100
XCTAssertEqual(
98101
userDefaults
99102
.dictionary(forKey: "com.kickstarter.KeyValueStoreType.remoteConfigFeatureFlags") as? [String: Bool],
100103
nil
101104
)
102105

103-
self.vm.inputs.setFeatureAtIndexEnabled(index: 0, isEnabled: true)
106+
self.vm.inputs.setFeatureAtIndexEnabled(index: index, isEnabled: true)
104107

105108
self.scheduler.advance()
106109

@@ -112,7 +115,7 @@ final class RemoteConfigFlagToolsViewModelTests: TestCase {
112115
userDefaults
113116
.dictionary(forKey: "com.kickstarter.KeyValueStoreType.remoteConfigFeatureFlags") as? [String: Bool],
114117
[
115-
RemoteConfigFeature.editPledgeOverTimeEnabled.rawValue: true
118+
feature.rawValue: true
116119
]
117120
)
118121
}

0 commit comments

Comments
 (0)