From 7101d33d1a9e286bb84043d6cf8976a727d281ca Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 11 May 2020 15:27:54 -0400 Subject: [PATCH 01/16] Added support for pre-determined / binary related ref codes. Now properly ping the `nonua` endpoint if device receives a ref code independent on the Brave API. --- BraveShared/Analytics/UrpService.swift | 32 +++++++++++++------ .../Analytics/UserReferralProgram.swift | 4 ++- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/BraveShared/Analytics/UrpService.swift b/BraveShared/Analytics/UrpService.swift index 3aa1f161e..c6d1a6134 100644 --- a/BraveShared/Analytics/UrpService.swift +++ b/BraveShared/Analytics/UrpService.swift @@ -13,8 +13,12 @@ enum UrpError { /// Api endpoints for user referral program. struct UrpService { - private static let apiKeyParam = "api_key" - private static let downLoadIdKeyParam = "download_id" + private struct ParamKeys { + static let api = "api_key" + static let referralCode = "referral_code" + static let platform = "platform" + static let downLoadId = "download_id" + } let host: String private let apiKey: String @@ -33,16 +37,24 @@ struct UrpService { sessionManager = URLSession(configuration: .default, delegate: certificateEvaluator, delegateQueue: .main) } - func referralCodeLookup(completion: @escaping (ReferralData?, UrpError?) -> Void) { + func referralCodeLookup(refCode: String?, completion: @escaping (ReferralData?, UrpError?) -> Void) { guard var endPoint = URL(string: host) else { completion(nil, .endpointError) UrpLog.log("Host not a url: \(host)") return } - endPoint.appendPathComponent("promo/initialize/ua") - - let params = [UrpService.apiKeyParam: apiKey] - + + var params = [UrpService.ParamKeys.api: apiKey] + + var lastPathComponent = "ua" + if let refCode = refCode { + params[UrpService.ParamKeys.referralCode] = refCode + params[UrpService.ParamKeys.platform] = "ios" + lastPathComponent = "nonua" + } + + endPoint.appendPathComponent("promo/initialize/\(lastPathComponent)") + sessionManager.urpApiRequest(endPoint: endPoint, params: params) { response in switch response { case .success(let data): @@ -70,8 +82,8 @@ struct UrpService { endPoint.appendPathComponent("promo/activity") let params = [ - UrpService.apiKeyParam: apiKey, - UrpService.downLoadIdKeyParam: downloadId + UrpService.ParamKeys.api: apiKey, + UrpService.ParamKeys.downLoadId: downloadId ] sessionManager.urpApiRequest(endPoint: endPoint, params: params) { response in @@ -95,7 +107,7 @@ struct UrpService { } endPoint.appendPathComponent("promo/custom-headers") - let params = [UrpService.apiKeyParam: apiKey] + let params = [UrpService.ParamKeys.api: apiKey] sessionManager.request(endPoint, parameters: params) { response in switch response { diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 6c7c95524..1df2e290b 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -47,7 +47,7 @@ public class UserReferralProgram { public func referralLookup(completion: @escaping (String?) -> Void) { UrpLog.log("first run referral lookup") - service.referralCodeLookup { referral, _ in + let referralBlock: (ReferralData?, UrpError?) -> Void = { referral, _ in guard let ref = referral else { log.info("No referral code found") UrpLog.log("No referral code found") @@ -78,6 +78,8 @@ public class UserReferralProgram { completion(nil) } + + service.referralCodeLookup(refCode: UserReferralProgram.getReferralCode(), completion: referralBlock) } private func initRetryPingConnection(numberOfTimes: Int32) { From 646e5bd8ac6ca2f6d8ce5f759f21ab935115e6b3 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 11 May 2020 16:14:06 -0400 Subject: [PATCH 02/16] Clipboard processing added. --- .../Analytics/UserReferralProgram.swift | 20 ++++++++++++++++++- .../Application/Delegates/AppDelegate.swift | 6 +++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 1df2e290b..a0338eb20 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -16,6 +16,7 @@ public class UserReferralProgram { public static let shared = UserReferralProgram() private static let apiKeyPlistKey = "API_KEY" + private static let clipboardPrefix = "F83AB73F-9852-4F01-ABA8-7830B8825993" struct HostUrl { static let staging = "https://brave-laptop-updates-staging.herokuapp.com" @@ -44,7 +45,7 @@ public class UserReferralProgram { } /// Looks for referral and returns its landing page if possible. - public func referralLookup(completion: @escaping (String?) -> Void) { + public func referralLookup(refCode: String?, completion: @escaping (String?) -> Void) { UrpLog.log("first run referral lookup") let referralBlock: (ReferralData?, UrpError?) -> Void = { referral, _ in @@ -79,6 +80,10 @@ public class UserReferralProgram { completion(nil) } + if let refCode = refCode { + Preferences.URP.referralCode.value = refCode + } + service.referralCodeLookup(refCode: UserReferralProgram.getReferralCode(), completion: referralBlock) } @@ -182,6 +187,19 @@ public class UserReferralProgram { return nil } + /// Passing string, attempts to derive a ref code from it. + /// Uses very strict matching. + /// Returns the sanitized code, or nil if no code was found + public class func sanitize(input: String?) -> String? { + guard var input = input, input.hasPrefix(self.clipboardPrefix) else { return nil } + + input.removeFirst(self.clipboardPrefix.count + 1) + // Add any other potential validation here, e.g. validating the actual ref code string + + if input.isEmpty { return nil } + return input + } + /// Same as `customHeaders` only blocking on result, to gaurantee data is available private func fetchNewCustomHeaders() -> Deferred<[CustomHeaderData]> { let result = Deferred<[CustomHeaderData]>() diff --git a/Client/Application/Delegates/AppDelegate.swift b/Client/Application/Delegates/AppDelegate.swift index 17d3ad745..06433ecdc 100644 --- a/Client/Application/Delegates/AppDelegate.swift +++ b/Client/Application/Delegates/AppDelegate.swift @@ -259,9 +259,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati FavoritesHelper.addDefaultFavorites() profile?.searchEngines.regionalSearchEngineSetup() } + if let urp = UserReferralProgram.shared { if isFirstLaunch { - urp.referralLookup { url in + let refCode = UserReferralProgram.sanitize(input: UIPasteboard.general.string) + + // This should be called each time, since if have + urp.referralLookup(refCode: refCode) { url in guard let url = url?.asURL else { return } self.browserViewController.openReferralLink(url: url) } From 624dabc6b2f9a24c4c3adeb5f747e5e859a23743 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 11 May 2020 18:39:11 -0400 Subject: [PATCH 03/16] Clear clipboard after successful ref promo read. --- Client/Application/Delegates/AppDelegate.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Client/Application/Delegates/AppDelegate.swift b/Client/Application/Delegates/AppDelegate.swift index 02bc055c4..064771edc 100644 --- a/Client/Application/Delegates/AppDelegate.swift +++ b/Client/Application/Delegates/AppDelegate.swift @@ -261,7 +261,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati if let urp = UserReferralProgram.shared { if isFirstLaunch { - let refCode = UserReferralProgram.sanitize(input: UIPasteboard.general.string) + let refCode = UserReferralProgram.sanitize(input: UIPasteboard.general.string) + if refCode != nil { UIPasteboard.general.clearPasteboard() } + urp.referralLookup(refCode: refCode) { referralCode, offerUrl in if let code = referralCode { let retryTime = AppConstants.buildChannel.isPublic ? 1.days : 10.minutes From 15bfead65cb6c749f9805723e0abe542cb8a3f59 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 18 May 2020 09:15:53 -0400 Subject: [PATCH 04/16] Cleaned up code a bit, no functional changes. --- BraveShared/Analytics/UserReferralProgram.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 05e7e6430..25d7dcfdd 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -82,6 +82,8 @@ public class UserReferralProgram { } if let refCode = refCode { + // This is also potentially set after server network request, + // esp important for binaries that require server ref code retrieval. Preferences.URP.referralCode.value = refCode } @@ -194,11 +196,11 @@ public class UserReferralProgram { public class func sanitize(input: String?) -> String? { guard var input = input, input.hasPrefix(self.clipboardPrefix) else { return nil } + // +1 to strip off `:` that proceeds the defined prefix input.removeFirst(self.clipboardPrefix.count + 1) // Add any other potential validation here, e.g. validating the actual ref code string - if input.isEmpty { return nil } - return input + return input.isEmpty ? nil : input } /// Same as `customHeaders` only blocking on result, to gaurantee data is available From 6fdb085b61de86041c0bee88a712365602ebc0c6 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 18 May 2020 13:02:36 -0400 Subject: [PATCH 05/16] Added ref code validation. --- .../Analytics/UserReferralProgram.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 25d7dcfdd..b0d94099c 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -194,13 +194,28 @@ public class UserReferralProgram { /// Uses very strict matching. /// Returns the sanitized code, or nil if no code was found public class func sanitize(input: String?) -> String? { - guard var input = input, input.hasPrefix(self.clipboardPrefix) else { return nil } + guard var code = input, code.hasPrefix(self.clipboardPrefix) else { return nil } // +1 to strip off `:` that proceeds the defined prefix - input.removeFirst(self.clipboardPrefix.count + 1) + code.removeFirst(self.clipboardPrefix.count + 1) // Add any other potential validation here, e.g. validating the actual ref code string - return input.isEmpty ? nil : input + if code.count != 6 { return nil } + let letters = code.prefix(3) + let numbers = code.suffix(3) + + // Cannot use `isLetters` or `isUppercase` b/inc Æ and the like + let validLetters = letters.allSatisfy(("A"..."Z").contains) + + // Cannot use `isNumber` b/inc 万 and the like + let validNumbers = numbers.allSatisfy(("0"..."9").contains) + + // Both conditions must be met + return validLetters && validNumbers ? code : nil + + // Regex solution +// let valid = code.range(of: #"\b[A-Z]{3}[0-9]{3}\b"#, options: .regularExpression) != nil // true + } /// Same as `customHeaders` only blocking on result, to gaurantee data is available From 8214d99dbc457f76e3ff9578b6064b383f7d7d20 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 18 May 2020 13:11:15 -0400 Subject: [PATCH 06/16] Changed ref code sanitzation to use regex. --- .../Analytics/UserReferralProgram.swift | 22 ++++---------- ClientTests/UserReferralProgramTests.swift | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 ClientTests/UserReferralProgramTests.swift diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index b0d94099c..8608c4f1e 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -194,28 +194,16 @@ public class UserReferralProgram { /// Uses very strict matching. /// Returns the sanitized code, or nil if no code was found public class func sanitize(input: String?) -> String? { - guard var code = input, code.hasPrefix(self.clipboardPrefix) else { return nil } + guard var input = input, input.hasPrefix(self.clipboardPrefix) else { return nil } // +1 to strip off `:` that proceeds the defined prefix - code.removeFirst(self.clipboardPrefix.count + 1) + input.removeFirst(self.clipboardPrefix.count + 1) // Add any other potential validation here, e.g. validating the actual ref code string - if code.count != 6 { return nil } - let letters = code.prefix(3) - let numbers = code.suffix(3) - - // Cannot use `isLetters` or `isUppercase` b/inc Æ and the like - let validLetters = letters.allSatisfy(("A"..."Z").contains) - - // Cannot use `isNumber` b/inc 万 and the like - let validNumbers = numbers.allSatisfy(("0"..."9").contains) - - // Both conditions must be met - return validLetters && validNumbers ? code : nil - - // Regex solution -// let valid = code.range(of: #"\b[A-Z]{3}[0-9]{3}\b"#, options: .regularExpression) != nil // true + let valid = input.range(of: #"\b[A-Z]{3}[0-9]{3}\b"#, options: .regularExpression) != nil // true + // Both conditions must be met + return valid ? input : nil } /// Same as `customHeaders` only blocking on result, to gaurantee data is available diff --git a/ClientTests/UserReferralProgramTests.swift b/ClientTests/UserReferralProgramTests.swift new file mode 100644 index 000000000..4312d4721 --- /dev/null +++ b/ClientTests/UserReferralProgramTests.swift @@ -0,0 +1,30 @@ +// Copyright 2020 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import XCTest + +class UserReferralProgramTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} From 285fe1ed9834ac77cc875179c212685041e53d88 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 18 May 2020 13:27:23 -0400 Subject: [PATCH 07/16] Added unit tests for ref code sanitization. --- .../UserReferralProgramTests.swift | 65 +++++++++++++++++++ Client.xcodeproj/project.pbxproj | 4 ++ ClientTests/UserReferralProgramTests.swift | 30 --------- 3 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 BraveSharedTests/UserReferralProgramTests.swift delete mode 100644 ClientTests/UserReferralProgramTests.swift diff --git a/BraveSharedTests/UserReferralProgramTests.swift b/BraveSharedTests/UserReferralProgramTests.swift new file mode 100644 index 000000000..dedc1f3a9 --- /dev/null +++ b/BraveSharedTests/UserReferralProgramTests.swift @@ -0,0 +1,65 @@ +// Copyright 2020 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import XCTest +@testable import BraveShared + + +class UserReferralProgramTests: XCTestCase { + + private static let clipboardPrefix = "F83AB73F-9852-4F01-ABA8-7830B8825993" + + func testSanitzerGoodInput() throws { + let response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):ABC123") + XCTAssertNotNil(response) + } + + func testSanitzerBadPrefix() throws { + let response = UserReferralProgram.sanitize(input: "BADPREFIX:ABC123") + XCTAssertNil(response) + } + + func testSanitzerBadCode() throws { + var response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):ABCA123") + XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):ABc123") + XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):ABC1234") + XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):ABC12#") + XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):ABC12") + XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):AB123") + XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):ÀBC123") + XCTAssertNil(response) + + } + + func testSanitzerNoInput() throws { + let response = UserReferralProgram.sanitize(input: nil) + XCTAssertNil(response) + } + + func testSanitzerEmptyInput() throws { + var response = UserReferralProgram.sanitize(input: "") + XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):") + XCTAssertNil(response) + } + + func testNoSpacer() throws { + let response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix)ABC123") + XCTAssertNil(response) + } +} diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 5aceeb9ba..1e1a90065 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -729,6 +729,7 @@ 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */; }; 78A9D0942407B3110077576C /* BraveGetUA.js in Resources */ = {isa = PBXBuildFile; fileRef = 78A9D0922407B3110077576C /* BraveGetUA.js */; }; 78A9D0962407B4A90077576C /* BraveGetUA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A9D0952407B4A90077576C /* BraveGetUA.swift */; }; + 78BA4B602472F9F900BD0897 /* UserReferralProgramTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BA4B5C2472F8C100BD0897 /* UserReferralProgramTests.swift */; }; 7B10AA9F1E3A15020002DD08 /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B10AA9E1E3A15020002DD08 /* DataExtensions.swift */; }; 7B10AABB1E3A1F650002DD08 /* URLRequestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B10AABA1E3A1F650002DD08 /* URLRequestExtensions.swift */; }; 7B2142FE1E5E055000CDD3FC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7B2142FC1E5E055000CDD3FC /* InfoPlist.strings */; }; @@ -2242,6 +2243,7 @@ 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsContentViewController.swift; sourceTree = ""; }; 78A9D0922407B3110077576C /* BraveGetUA.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = BraveGetUA.js; sourceTree = ""; }; 78A9D0952407B4A90077576C /* BraveGetUA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BraveGetUA.swift; sourceTree = ""; }; + 78BA4B5C2472F8C100BD0897 /* UserReferralProgramTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserReferralProgramTests.swift; sourceTree = ""; }; 7B10AA9E1E3A15020002DD08 /* DataExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = ""; }; 7B10AABA1E3A1F650002DD08 /* URLRequestExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestExtensions.swift; sourceTree = ""; }; 7B2142FD1E5E055000CDD3FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -4247,6 +4249,7 @@ 5965CA212215D4A0000B9EC1 /* CustomHeaderDataTests.swift */, 2777273F22EB44B300F0214C /* StringExtensionTests.swift */, 5EEB25DD234E667700279091 /* CertificatePinningTest.swift */, + 78BA4B5C2472F8C100BD0897 /* UserReferralProgramTests.swift */, ); path = BraveSharedTests; sourceTree = ""; @@ -6311,6 +6314,7 @@ files = ( A176323C20CF2AF900126F25 /* DeferredTestUtils.swift in Sources */, 5DE7689420B3456E00FF5533 /* BraveSharedTests.swift in Sources */, + 78BA4B602472F9F900BD0897 /* UserReferralProgramTests.swift in Sources */, 2777274022EB44B300F0214C /* StringExtensionTests.swift in Sources */, 27A586E1214C0DDD000CAE3C /* PreferencesTest.swift in Sources */, 5965CA222215D4A0000B9EC1 /* CustomHeaderDataTests.swift in Sources */, diff --git a/ClientTests/UserReferralProgramTests.swift b/ClientTests/UserReferralProgramTests.swift deleted file mode 100644 index 4312d4721..000000000 --- a/ClientTests/UserReferralProgramTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2020 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import XCTest - -class UserReferralProgramTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} From aaa762e381f175fe031574dbff23d508fea2c944 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 18 May 2020 13:44:16 -0400 Subject: [PATCH 08/16] Removed unnecessary comment. --- BraveShared/Analytics/UserReferralProgram.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 8608c4f1e..3df1ca795 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -198,8 +198,6 @@ public class UserReferralProgram { // +1 to strip off `:` that proceeds the defined prefix input.removeFirst(self.clipboardPrefix.count + 1) - // Add any other potential validation here, e.g. validating the actual ref code string - let valid = input.range(of: #"\b[A-Z]{3}[0-9]{3}\b"#, options: .regularExpression) != nil // true // Both conditions must be met From 16d5037c3d3bf27581b4efa39cce6ca6d978ce91 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Mon, 18 May 2020 16:17:23 -0400 Subject: [PATCH 09/16] Added path components helper to avoid manually adding forward slashes. --- BraveShared/Analytics/UrpService.swift | 7 +++---- Shared/Extensions/URLExtensions.swift | 6 ++++++ SharedTests/NSURLExtensionsTests.swift | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/BraveShared/Analytics/UrpService.swift b/BraveShared/Analytics/UrpService.swift index c6d1a6134..8e86b927b 100644 --- a/BraveShared/Analytics/UrpService.swift +++ b/BraveShared/Analytics/UrpService.swift @@ -52,8 +52,7 @@ struct UrpService { params[UrpService.ParamKeys.platform] = "ios" lastPathComponent = "nonua" } - - endPoint.appendPathComponent("promo/initialize/\(lastPathComponent)") + endPoint.append(pathComponents: "promo", "initialize", lastPathComponent) sessionManager.urpApiRequest(endPoint: endPoint, params: params) { response in switch response { @@ -79,7 +78,7 @@ struct UrpService { completion(nil, .endpointError) return } - endPoint.appendPathComponent("promo/activity") + endPoint.append(pathComponents: "promo", "activity") let params = [ UrpService.ParamKeys.api: apiKey, @@ -105,7 +104,7 @@ struct UrpService { completion([], .endpointError) return } - endPoint.appendPathComponent("promo/custom-headers") + endPoint.append(pathComponents: "promo", "custom-headers") let params = [UrpService.ParamKeys.api: apiKey] diff --git a/Shared/Extensions/URLExtensions.swift b/Shared/Extensions/URLExtensions.swift index 3a037a9d7..51e5f39bd 100644 --- a/Shared/Extensions/URLExtensions.swift +++ b/Shared/Extensions/URLExtensions.swift @@ -82,6 +82,12 @@ extension URL { } return val } + + mutating public func append(pathComponents: String...) { + pathComponents.forEach { + self.appendPathComponent($0) + } + } public func getResourceLongLongForKey(_ key: String) -> Int64? { return (getResourceValueForKey(key) as? NSNumber)?.int64Value diff --git a/SharedTests/NSURLExtensionsTests.swift b/SharedTests/NSURLExtensionsTests.swift index 6d30d92d1..517d6279a 100644 --- a/SharedTests/NSURLExtensionsTests.swift +++ b/SharedTests/NSURLExtensionsTests.swift @@ -476,6 +476,21 @@ class NSURLExtensionsTests: XCTestCase { XCTAssertEqual("http://bar.com/noo?qqq=123", urlD.absoluteString) XCTAssertEqual("http://foo.com/bar/?ppp=123&rrr=aaa", urlE.absoluteString) } + + func testAppendPathComponentsHelper() { + var urlA = URL(string: "http://foo.com/bar/")! + var urlB = URL(string: "http://bar.com/noo")! + + urlA.append(pathComponents: "foo") + urlA.append(pathComponents: "one", "two") + + urlB.append(pathComponents: "", "", "test", "", "one", "") + + XCTAssertEqual("http://foo.com/bar/foo/one/two", urlA.absoluteString) + XCTAssertEqual("http://bar.com/noo/test/one/", urlB.absoluteString) + + + } func testHidingFromDataDetectors() { guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { From 576ae4a30275adfb10b469ddfcb8d652959121f4 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Wed, 20 May 2020 13:07:25 -0400 Subject: [PATCH 10/16] Comment cleanup. --- BraveShared/Analytics/UserReferralProgram.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 3df1ca795..73049a56c 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -87,6 +87,8 @@ public class UserReferralProgram { Preferences.URP.referralCode.value = refCode } + // Since ref-code method may not be repeatable (e.g. clipboard was cleared), this should be retrieved from prefs, + // and not use the passed in referral code. service.referralCodeLookup(refCode: UserReferralProgram.getReferralCode(), completion: referralBlock) } @@ -198,7 +200,7 @@ public class UserReferralProgram { // +1 to strip off `:` that proceeds the defined prefix input.removeFirst(self.clipboardPrefix.count + 1) - let valid = input.range(of: #"\b[A-Z]{3}[0-9]{3}\b"#, options: .regularExpression) != nil // true + let valid = input.range(of: #"\b[A-Z]{3}[0-9]{3}\b"#, options: .regularExpression) != nil // Both conditions must be met return valid ? input : nil From 834a1fcd47d7ae55cac2d571a5cf34ed83216351 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Wed, 20 May 2020 13:12:43 -0400 Subject: [PATCH 11/16] Fixed crash from bad referral situation. --- BraveShared/Analytics/UserReferralProgram.swift | 6 +++++- BraveSharedTests/UserReferralProgramTests.swift | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 73049a56c..85a355550 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -196,7 +196,11 @@ public class UserReferralProgram { /// Uses very strict matching. /// Returns the sanitized code, or nil if no code was found public class func sanitize(input: String?) -> String? { - guard var input = input, input.hasPrefix(self.clipboardPrefix) else { return nil } + guard + var input = input, + input.hasPrefix(self.clipboardPrefix), + input.count > self.clipboardPrefix.count + else { return nil } // +1 to strip off `:` that proceeds the defined prefix input.removeFirst(self.clipboardPrefix.count + 1) diff --git a/BraveSharedTests/UserReferralProgramTests.swift b/BraveSharedTests/UserReferralProgramTests.swift index dedc1f3a9..0cd36e8fe 100644 --- a/BraveSharedTests/UserReferralProgramTests.swift +++ b/BraveSharedTests/UserReferralProgramTests.swift @@ -56,6 +56,9 @@ class UserReferralProgramTests: XCTestCase { response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix):") XCTAssertNil(response) + + response = UserReferralProgram.sanitize(input: "\(UserReferralProgramTests.clipboardPrefix)") + XCTAssertNil(response) } func testNoSpacer() throws { From 98253b6c50522a7c20293658549923e66a7d95fd Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Thu, 21 May 2020 12:58:04 -0400 Subject: [PATCH 12/16] Fixed S3 bucket endpoint for enterprise builds. --- BraveShared/Analytics/UserReferralProgram.swift | 1 + Client/Frontend/Browser/HomePanel/NTPDownloader.swift | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 85a355550..3aebe1cfa 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -30,6 +30,7 @@ public class UserReferralProgram { return Bundle.main.infoDictionary?[key] as? String } + // This should _probably_ correspond to the baseUrl for NTPDownloader let host = AppConstants.buildChannel == .developer ? HostUrl.staging : HostUrl.prod guard let apiKey = getPlistString(for: UserReferralProgram.apiKeyPlistKey)?.trimmingCharacters(in: .whitespacesAndNewlines) else { diff --git a/Client/Frontend/Browser/HomePanel/NTPDownloader.swift b/Client/Frontend/Browser/HomePanel/NTPDownloader.swift index 747740dc2..2efe3f6c3 100644 --- a/Client/Frontend/Browser/HomePanel/NTPDownloader.swift +++ b/Client/Frontend/Browser/HomePanel/NTPDownloader.swift @@ -22,8 +22,10 @@ class NTPDownloader { case sponsor var resourceBaseURL: URL? { - let baseUrl = AppConstants.buildChannel.isPublic ? "https://mobile-data.s3.brave.com/" - : "https://mobile-data-dev.s3.brave.software" + // This should _probably_ correspond host for URP + let baseUrl = AppConstants.buildChannel == .developer + ? "https://mobile-data-dev.s3.brave.software" + : "https://mobile-data.s3.brave.com/" switch self { case .superReferral(let code): From 411481f06bfa6548c1223b50f9505d65a9288540 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Fri, 22 May 2020 16:42:22 -0400 Subject: [PATCH 13/16] Fix #2565: Retry ref code lookup on subsequent launches. --- BraveShared/Analytics/UserReferralProgram.swift | 1 + BraveShared/Preferences.swift | 2 ++ Client/Application/Delegates/AppDelegate.swift | 17 +++++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 3aebe1cfa..4d9a9a07a 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -47,6 +47,7 @@ public class UserReferralProgram { /// Looks for referral and returns its landing page if possible. public func referralLookup(refCode: String?, completion: @escaping (_ refCode: String?, _ offerUrl: String?) -> Void) { + Preferences.URP.referralLookupOutstanding.value = false UrpLog.log("first run referral lookup") let referralBlock: (ReferralData?, UrpError?) -> Void = { referral, _ in diff --git a/BraveShared/Preferences.swift b/BraveShared/Preferences.swift index e65961fba..e665ad7d6 100644 --- a/BraveShared/Preferences.swift +++ b/BraveShared/Preferences.swift @@ -41,6 +41,8 @@ extension Preferences { static let downloadId = Option(key: "urp.referral.download-id", default: nil) public static let referralCode = Option(key: "urp.referral.code", default: nil) static let referralCodeDeleteDate = Option(key: "urp.referral.delete-date", default: nil) + /// Whether the ref code lookup has still yet to occur + public static let referralLookupOutstanding = Option(key: "urp.referral.lookkup-completed", default: nil) } public final class NTP { diff --git a/Client/Application/Delegates/AppDelegate.swift b/Client/Application/Delegates/AppDelegate.swift index 064771edc..a6851c278 100644 --- a/Client/Application/Delegates/AppDelegate.swift +++ b/Client/Application/Delegates/AppDelegate.swift @@ -260,9 +260,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati } if let urp = UserReferralProgram.shared { - if isFirstLaunch { + if Preferences.URP.referralLookupOutstanding.value == nil { + // This preference has never been set, and this means it is a new or upgraded user. + // That distinction must be made to know if a network request for ref-code look up should be made. + + // Setting this to an explicit value so it will never get overwritten on subsequent launches. + // Upgrade users should not have ref code ping happening. + Preferences.URP.referralLookupOutstanding.value = isFirstLaunch + } + + if Preferences.URP.referralLookupOutstanding.value == true { let refCode = UserReferralProgram.sanitize(input: UIPasteboard.general.string) - if refCode != nil { UIPasteboard.general.clearPasteboard() } + if refCode != nil { + UrpLog.log("Clipboard ref code found: " + (UIPasteboard.general.string ?? "!Clipboard Empty!")) + UrpLog.log("Clearing clipboard.") + UIPasteboard.general.clearPasteboard() + } urp.referralLookup(refCode: refCode) { referralCode, offerUrl in if let code = referralCode { From aae0eb8235e878dacf9e1a34ba8e906fb220c2d4 Mon Sep 17 00:00:00 2001 From: Joel Reis Date: Tue, 26 May 2020 12:18:47 -0400 Subject: [PATCH 14/16] Fixed URP lookup flag placement. --- BraveShared/Analytics/UserReferralProgram.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 4d9a9a07a..882431d92 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -47,10 +47,17 @@ public class UserReferralProgram { /// Looks for referral and returns its landing page if possible. public func referralLookup(refCode: String?, completion: @escaping (_ refCode: String?, _ offerUrl: String?) -> Void) { - Preferences.URP.referralLookupOutstanding.value = false UrpLog.log("first run referral lookup") - let referralBlock: (ReferralData?, UrpError?) -> Void = { referral, _ in + let referralBlock: (ReferralData?, UrpError?) -> Void = { referral, error in + if error == BraveShared.UrpError.endpointError { + UrpLog.log("URP look up had endpoint error, will retry on next launch.") + return + } + + // Connection "succeeded" + + Preferences.URP.referralLookupOutstanding.value = false guard let ref = referral else { log.info("No referral code found") UrpLog.log("No referral code found") From 92f0562b56eaa5be60feab908ecc5fefb1f08931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=C2=A0Buczek?= Date: Wed, 27 May 2020 17:32:17 +0200 Subject: [PATCH 15/16] Add retry timer. --- .../Analytics/UserReferralProgram.swift | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 882431d92..1e08b7b38 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -23,6 +23,13 @@ public class UserReferralProgram { static let prod = "https://laptop-updates.brave.com" } + // In case of network problems when looking for referrral code + // we retry the call few times while the app is still alive. + private var referralLookupRetryTimer: Timer? + private var referralLookupCurrentCount = 0 + private let referralLookupRetryMaxCount = 10 + private let referralLookupRetryTimeInterval = AppConstants.buildChannel.isPublic ? 3.minutes : 1.minutes + let service: UrpService public init?() { @@ -49,9 +56,25 @@ public class UserReferralProgram { public func referralLookup(refCode: String?, completion: @escaping (_ refCode: String?, _ offerUrl: String?) -> Void) { UrpLog.log("first run referral lookup") - let referralBlock: (ReferralData?, UrpError?) -> Void = { referral, error in + let referralBlock: (ReferralData?, UrpError?) -> Void = { [weak self] referral, error in + guard let self = self else { return } + if error == BraveShared.UrpError.endpointError { UrpLog.log("URP look up had endpoint error, will retry on next launch.") + self.referralLookupRetryTimer?.invalidate() + self.referralLookupRetryTimer = nil + + // Hit max retry attempts. + if self.referralLookupCurrentCount > self.referralLookupRetryMaxCount { return } + + self.referralLookupCurrentCount += 1 + self.referralLookupRetryTimer = + Timer.scheduledTimer(withTimeInterval: self.referralLookupRetryTimeInterval, + repeats: true) { [weak self] _ in + self?.referralLookup(refCode: refCode) { refCode, offerUrl in + completion(refCode, offerUrl) + } + } return } @@ -83,6 +106,9 @@ public class UserReferralProgram { Preferences.URP.downloadId.value = ref.downloadId Preferences.URP.referralCode.value = ref.referralCode + self.referralLookupRetryTimer?.invalidate() + self.referralLookupRetryTimer = nil + UrpLog.log("Found referral: downloadId: \(ref.downloadId), code: \(ref.referralCode)") // In case of network errors or getting `isFinalized = false`, we retry the api call. self.initRetryPingConnection(numberOfTimes: 30) From 78d2f752ee279a1dd7251f435a86dc8e8840475b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=C2=A0Buczek?= Date: Wed, 27 May 2020 20:26:06 +0200 Subject: [PATCH 16/16] Move to struct. --- .../Analytics/UserReferralProgram.swift | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/BraveShared/Analytics/UserReferralProgram.swift b/BraveShared/Analytics/UserReferralProgram.swift index 1e08b7b38..7240bf550 100644 --- a/BraveShared/Analytics/UserReferralProgram.swift +++ b/BraveShared/Analytics/UserReferralProgram.swift @@ -25,10 +25,14 @@ public class UserReferralProgram { // In case of network problems when looking for referrral code // we retry the call few times while the app is still alive. - private var referralLookupRetryTimer: Timer? - private var referralLookupCurrentCount = 0 - private let referralLookupRetryMaxCount = 10 - private let referralLookupRetryTimeInterval = AppConstants.buildChannel.isPublic ? 3.minutes : 1.minutes + private struct ReferralLookupRetry { + var timer: Timer? + var currentCount = 0 + let retryLimit = 10 + let retryTimeInterval = AppConstants.buildChannel.isPublic ? 3.minutes : 1.minutes + } + + private var referralLookupRetry = ReferralLookupRetry() let service: UrpService @@ -61,15 +65,15 @@ public class UserReferralProgram { if error == BraveShared.UrpError.endpointError { UrpLog.log("URP look up had endpoint error, will retry on next launch.") - self.referralLookupRetryTimer?.invalidate() - self.referralLookupRetryTimer = nil + self.referralLookupRetry.timer?.invalidate() + self.referralLookupRetry.timer = nil // Hit max retry attempts. - if self.referralLookupCurrentCount > self.referralLookupRetryMaxCount { return } + if self.referralLookupRetry.currentCount > self.referralLookupRetry.retryLimit { return } - self.referralLookupCurrentCount += 1 - self.referralLookupRetryTimer = - Timer.scheduledTimer(withTimeInterval: self.referralLookupRetryTimeInterval, + self.referralLookupRetry.currentCount += 1 + self.referralLookupRetry.timer = + Timer.scheduledTimer(withTimeInterval: self.referralLookupRetry.retryTimeInterval, repeats: true) { [weak self] _ in self?.referralLookup(refCode: refCode) { refCode, offerUrl in completion(refCode, offerUrl) @@ -106,8 +110,8 @@ public class UserReferralProgram { Preferences.URP.downloadId.value = ref.downloadId Preferences.URP.referralCode.value = ref.referralCode - self.referralLookupRetryTimer?.invalidate() - self.referralLookupRetryTimer = nil + self.referralLookupRetry.timer?.invalidate() + self.referralLookupRetry.timer = nil UrpLog.log("Found referral: downloadId: \(ref.downloadId), code: \(ref.referralCode)") // In case of network errors or getting `isFinalized = false`, we retry the api call.